diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..ed13dfa68 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[report] +omit=src/yunohost/tests/*,src/yunohost/vendor/*,/usr/lib/moulinette/yunohost/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eb34de38e..d1cb36b73 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,11 @@ +--- stages: - build - install - tests - lint - doc + - translation default: tags: @@ -11,12 +13,18 @@ default: # All jobs are interruptible by default interruptible: true +# see: https://docs.gitlab.com/ee/ci/yaml/#switch-between-branch-pipelines-and-merge-request-pipelines +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" # If we move to gitlab one day + - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" # For github PR + - if: $CI_COMMIT_TAG # For tags + - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" # If it's not the default branch and if it's a push, then do not trigger a build + when: never + - when: always + variables: - YNH_BUILD_DIR: "ynh-build" + YNH_BUILD_DIR: "ynh-build" include: - - local: .gitlab/ci/build.gitlab-ci.yml - - local: .gitlab/ci/install.gitlab-ci.yml - - local: .gitlab/ci/test.gitlab-ci.yml - - local: .gitlab/ci/lint.gitlab-ci.yml - - local: .gitlab/ci/doc.gitlab-ci.yml + - local: .gitlab/ci/*.gitlab-ci.yml diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 7227b8acb..59179f7a7 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -12,9 +12,9 @@ generate-helpers-doc: - git config --global user.name "$GITHUB_USER" script: - cd doc - - python generate_helper_doc.py + - python3 generate_helper_doc.py - hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo - - cp helpers.html doc_repo/packaging_apps_helpers.md + - cp helpers.md doc_repo/pages/04.contribute/04.packaging_apps/11.helpers/packaging_apps_helpers.md - cd doc_repo # replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ? - hub checkout -b "${CI_COMMIT_REF_NAME}" @@ -22,6 +22,6 @@ generate-helpers-doc: - hub pull-request -m "[CI] Helper for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd artifacts: paths: - - doc/helpers.html + - doc/helpers.md only: - tags diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 72faaaf2c..aaddb5a0a 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -3,6 +3,7 @@ ######################################## # later we must fix lint and format-check jobs and remove "allow_failure" +--- lint37: stage: lint image: "before-install" @@ -18,6 +19,13 @@ invalidcode37: script: - tox -e py37-invalidcode +mypy: + stage: lint + image: "before-install" + needs: [] + script: + - tox -e py37-mypy + format-check: stage: lint image: "before-install" @@ -37,11 +45,13 @@ format-run: - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo - cd github_repo script: - # checkout or create and checkout the branch - - hub checkout "ci-format-${CI_COMMIT_REF_NAME}" || hub checkout -b "ci-format-${CI_COMMIT_REF_NAME}" + # create a local branch that will overwrite distant one + - git checkout -b "ci-format-${CI_COMMIT_REF_NAME}" --no-track - tox -e py37-black-run - - hub commit -am "[CI] Format code" || true + - '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit + - git commit -am "[CI] Format code" || true + - git push -f origin "ci-format-${CI_COMMIT_REF_NAME}":"ci-format-${CI_COMMIT_REF_NAME}" - hub pull-request -m "[CI] Format code" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd only: - refs: - - dev \ No newline at end of file + refs: + - dev diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index a4ec77ee8..1aad46fbe 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -36,7 +36,9 @@ full-tests: - *install_debs - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace script: - - python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml + - python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ data/hooks/diagnosis/ --junitxml=report.xml + - cd tests + - bash test_helpers.sh needs: - job: build-yunohost artifacts: true @@ -48,73 +50,159 @@ full-tests: reports: junit: report.xml -root-tests: +test-i18n-keys: extends: .test-stage script: - - python3 -m pytest tests + - python3 -m pytest tests/test_i18n_keys.py + only: + changes: + - locales/en.json + - src/yunohost/*.py + - data/hooks/diagnosis/*.py + +test-translation-format-consistency: + extends: .test-stage + script: + - python3 -m pytest tests/test_translation_format_consistency.py + only: + changes: + - locales/* + +test-actionmap: + extends: .test-stage + script: + - python3 -m pytest tests/test_actionmap.py + only: + changes: + - data/actionsmap/*.yml + +test-helpers: + extends: .test-stage + script: + - cd tests + - bash test_helpers.sh + only: + changes: + - data/helpers.d/* + +test-domains: + extends: .test-stage + script: + - python3 -m pytest src/yunohost/tests/test_domains.py + only: + changes: + - src/yunohost/domain.py + +test-dns: + extends: .test-stage + script: + - python3 -m pytest src/yunohost/tests/test_dns.py + only: + changes: + - src/yunohost/dns.py + - src/yunohost/utils/dns.py test-apps: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_apps.py + - python3 -m pytest src/yunohost/tests/test_apps.py + only: + changes: + - src/yunohost/app.py test-appscatalog: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_appscatalog.py + - python3 -m pytest src/yunohost/tests/test_app_catalog.py + only: + changes: + - src/yunohost/app_calalog.py test-appurl: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_appurl.py + - python3 -m pytest src/yunohost/tests/test_appurl.py + only: + changes: + - src/yunohost/app.py -test-apps-arguments-parsing: +test-questions: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_apps_arguments_parsing.py + - python3 -m pytest src/yunohost/tests/test_questions.py + only: + changes: + - src/yunohost/utils/config.py -test-backuprestore: +test-app-config: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_backuprestore.py + - python3 -m pytest src/yunohost/tests/test_app_config.py + only: + changes: + - src/yunohost/app.py + - src/yunohost/utils/config.py test-changeurl: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_changeurl.py + - python3 -m pytest src/yunohost/tests/test_changeurl.py + only: + changes: + - src/yunohost/app.py + +test-backuprestore: + extends: .test-stage + script: + - python3 -m pytest src/yunohost/tests/test_backuprestore.py + only: + changes: + - src/yunohost/backup.py test-permission: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_permission.py + - python3 -m pytest src/yunohost/tests/test_permission.py + only: + changes: + - src/yunohost/permission.py test-settings: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_settings.py + - python3 -m pytest src/yunohost/tests/test_settings.py + only: + changes: + - src/yunohost/settings.py test-user-group: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_user-group.py - + - python3 -m pytest src/yunohost/tests/test_user-group.py + only: + changes: + - src/yunohost/user.py + test-regenconf: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_regenconf.py + - python3 -m pytest src/yunohost/tests/test_regenconf.py + only: + changes: + - src/yunohost/regenconf.py test-service: extends: .test-stage script: - - cd src/yunohost - - python3 -m pytest tests/test_service.py + - python3 -m pytest src/yunohost/tests/test_service.py + only: + changes: + - src/yunohost/service.py + +test-ldapauth: + extends: .test-stage + script: + - python3 -m pytest src/yunohost/tests/test_ldapauth.py + only: + changes: + - src/yunohost/authenticators/*.py diff --git a/.gitlab/ci/translation.gitlab-ci.yml b/.gitlab/ci/translation.gitlab-ci.yml new file mode 100644 index 000000000..41e8c82d2 --- /dev/null +++ b/.gitlab/ci/translation.gitlab-ci.yml @@ -0,0 +1,29 @@ +######################################## +# TRANSLATION +######################################## + +autofix-translated-strings: + stage: translation + image: "before-install" + needs: [] + before_script: + - apt-get update -y && apt-get install git hub -y + - git config --global user.email "yunohost@yunohost.org" + - git config --global user.name "$GITHUB_USER" + - git remote set-url origin https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git + script: + - cd tests # Maybe move this script location to another folder? + # create a local branch that will overwrite distant one + - 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 + - git push -f origin "HEAD":"ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}" + - hub pull-request -m "[CI] Reformat / remove stale translated strings" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + only: + variables: + - $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + changes: + - locales/* diff --git a/README.md b/README.md index aec5300e2..df3a4bb9f 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@
-
+
Doc auto-generated by this script on {{data.date}} (Yunohost version {{data.version}})
- -{% for category, helpers in data.helpers %} - -
- {% if not '\n' in h.usage %}
- Usage: {{ h.usage }}
- {% else %}
- Usage: {{ h.usage }}
- {% endif %}
-
- Arguments: -
{{ infos[0] }}
: {{ infos[1] }}{{ infos[0] }}
, {{ infos[1] }}
: {{ infos[2] }}- Returns: {{ h.ret }} -
- {% endif %} - {% if "example" in h.keys() %} -
- Example: {{ h.example }}
-
- Examples:
{{ example }}
- {% else %}
- {{ example.strip("# ") }}
- {% endif %}
- - Details: -
- {{ h.details.replace('\n', '') }} -
- - {% endif %} - - -{global}
",
"diagnosis_ip_local": "IP local: {local}
",
"diagnosis_dns_point_to_doc": "Consulteu la documentació a https://yunohost.org/dns_config si necessiteu ajuda per configurar els registres DNS.",
@@ -634,7 +529,7 @@
"diagnosis_ports_partially_unreachable": "El port {port} no és accessible des de l'exterior amb IPv{failed}.",
"diagnosis_http_partially_unreachable": "El domini {domain} sembla que no és accessible utilitzant HTTP des de l'exterior de la xarxa local amb IPv{failed}, tot i que funciona amb IPv{passed}.",
"diagnosis_mail_fcrdns_nok_details": "Hauríeu d'intentar configurar primer el DNS invers amb {ehlo_domain}
en la interfície del router o en la interfície del vostre allotjador. (Alguns allotjadors requereixen que obris un informe de suport per això).",
- "diagnosis_mail_fcrdns_nok_alternatives_4": "Alguns proveïdors no permeten configurar el DNS invers (o aquesta funció pot no funcionar…). Si teniu problemes a causa d'això, considereu les solucions següents:/etc/resolv.conf> kein Eintrag auf 127.0.0.1
zeigt.",
+ "diagnosis_ip_broken_resolvconf": "Domänen-Namensauflösung scheint nicht zu funktionieren, was daran liegen könnte, dass in /etc/resolv.conf
kein Eintrag auf 127.0.0.1
zeigt.",
"diagnosis_ip_weird_resolvconf_details": "Die Datei /etc/resolv.conf
muss ein Symlink auf /etc/resolvconf/run/resolv.conf
sein, welcher auf 127.0.0.1
(dnsmasq) zeigt. Falls Sie die DNS-Resolver manuell konfigurieren möchten, bearbeiten Sie bitte /etc/resolv.dnsmasq.conf
.",
"diagnosis_dns_good_conf": "Die DNS-Einträge für die Domäne {domain} (Kategorie {category}) sind korrekt konfiguriert",
"diagnosis_ignored_issues": "(+ {nb_ignored} ignorierte(s) Problem(e))",
"diagnosis_basesystem_hardware": "Server Hardware Architektur ist {virt} {arch}",
- "diagnosis_basesystem_hardware_board": "Server Platinen Modell ist {model}",
"diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!",
"diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.",
"diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!",
- "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie eine eigene /etc/resolv.conf
verwendest.",
- "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, können Sie zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues' in der Kommandozeile ausführen.",
+ "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie Ihren eigenen /etc/resolv.conf
verwenden.",
+ "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, können Sie zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues --human-readable' in der Kommandozeile ausführen.",
"backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}",
"backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).",
"app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Denken Sie darüber nach das System zu aktualisieren.",
- "certmanager_domain_not_diagnosed_yet": "Für {domain} gibt es noch keine Diagnose-Resultate. Bitte wiederholen Sie die Diagnose für die Kategorien 'DNS records' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domain für Let's Encrypt bereit ist. (Oder wenn Sie wissen was Sie tun, verwenden Sie '--no-checks' um diese Überprüfungen abzuschalten.",
+ "certmanager_domain_not_diagnosed_yet": "Für die Domain {domain} gibt es noch keine Diagnose-Resultate. Bitte widerhole die Diagnose für die Kategorien 'DNS records' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domain für Let's Encrypt bereit ist. (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen.)",
"migration_0015_patching_sources_list": "sources.lists wird repariert...",
"migration_0015_start": "Start der Migration auf Buster",
- "migration_0011_failed_to_remove_stale_object": "Abgelaufenes Objekt konne nicht entfernt werden. {dn}: {error}",
- "migration_0011_update_LDAP_schema": "Das LDAP-Schema aktualisieren...",
- "migration_0011_update_LDAP_database": "Die LDAP-Datenbank aktualisieren...",
- "migration_0011_migrate_permission": "Berechtigungen der Applikationen von den Einstellungen zu LDAP migrieren...",
- "migration_0011_LDAP_update_failed": "LDAP konnte nicht aktualisiert werden. Fehler:n{error:s}",
- "migration_0011_create_group": "Eine Gruppe für jeden Benutzer erstellen…",
"migration_description_0015_migrate_to_buster": "Auf Debian Buster und YunoHost 4.x upgraden",
"mail_unavailable": "Diese E-Mail Adresse ist reserviert und wird dem ersten Benutzer automatisch zugewiesen",
"diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!",
"diagnosis_services_running": "Dienst {service} läuft!",
"diagnosis_domain_expires_in": "{domain} läuft in {days} Tagen ab.",
"diagnosis_domain_expiration_error": "Einige Domänen werden SEHR BALD ablaufen!",
- "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.",
+ "diagnosis_domain_expiration_success": "Ihre Domänen sind registriert und werden in nächster Zeit nicht ablaufen.",
"diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!",
- "diagnosis_domain_expiration_not_found": "Konnte die Ablaufdaten für einige Domänen nicht überprüfen.",
- "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von Yunohost verwaltet werden. Andernfalls können Sie mittels yunohost dyndns update --force ein Update erzwingen.",
+ "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden",
+ "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domain sollte automatisch von YunoHost verwaltet werden. Andernfalls können Sie mittels yunohost dyndns update --force ein Update erzwingen.",
"diagnosis_dns_point_to_doc": "Bitte schauen Sie in die Dokumentation unter https://yunohost.org/dns_config wenn Sie Hilfe bei der Konfiguration der DNS-Einträge brauchen.",
"diagnosis_dns_discrepancy": "Der folgende DNS-Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}
",
"diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration sollten Sie einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}
",
@@ -372,12 +331,12 @@
"diagnosis_services_bad_status": "Der Dienst {service} ist {status} :(",
"diagnosis_diskusage_verylow": "Der Speicher {mountpoint}
(auf Gerät {device}
) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Sie sollten sich ernsthaft überlegen, einigen Seicherplatz frei zu machen!",
"diagnosis_http_ok": "Die Domäne {domain} ist über HTTP von außerhalb des lokalen Netzwerks erreichbar.",
- "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, da diese sich nicht um die Netzneutralität kümmern.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine die Privatsphäre berücksichtigende Alternative ist die Verwendung eines VPN *mit einer dedizierten öffentlichen IP* um solche Einschränkungen zu umgehen. Schauen Sie unter https://yunohost.org/#/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln.",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, da diese sich nicht um die Netzneutralität kümmern.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine die Privatsphäre berücksichtigende Alternative ist die Verwendung eines VPN *mit einer dedizierten öffentlichen IP* um solche Einschränkungen zu umgehen. Schauen Sie unter https://yunohost.org/#/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln",
"diagnosis_http_timeout": "Wartezeit wurde beim Versuch, von außen eine Verbindung zum Server aufzubauen, überschritten. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist daß der Port 80 (und 433) nicht richtig zu Ihrem Server weitergeleitet werden.
2. Sie sollten auch sicherstellen, daß der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stellen Sie sicher, daß keine Firewall oder Reverse-Proxy stört.",
- "service_reloaded_or_restarted": "Der Dienst '{service:s}' wurde erfolgreich neu geladen oder gestartet",
- "service_restarted": "Der Dienst '{service:s}' wurde neu gestartet",
+ "service_reloaded_or_restarted": "Der Dienst '{service}' wurde erfolgreich neu geladen oder gestartet",
+ "service_restarted": "Der Dienst '{service}' wurde neu gestartet",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' ist veraltet! Bitte verwenden Sie stattdessen 'yunohost tools regen-conf'.",
- "certmanager_warning_subdomain_dns_record": "Die Subdomain '{subdomain:s}' löst nicht dieselbe IP wie '{domain:s} auf. Einige Funktionen werden nicht verfügbar sein, solange Sie dies nicht beheben und das Zertifikat erneuern.",
+ "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain}\" löst nicht zur gleichen IP Adresse auf wie \"{domain}\". Einige Funktionen sind nicht verfügbar bis du dies behebst und die Zertifikate neu erzeugst.",
"diagnosis_ports_ok": "Port {port} ist von außen erreichbar.",
"diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})",
"diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen den ausgehenden Port 25 auf Ihrer Router-Konfigurationsoberfläche oder Ihrer Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbieter kann es sein, daß Sie verlangen, daß man dafür ein Support-Ticket sendet).",
@@ -391,8 +350,8 @@
"diagnosis_mail_ehlo_unreachable": "Der SMTP-Server ist von außen nicht erreichbar per IPv{ipversion}. Er wird nicht in der Lage sein E-Mails zu empfangen.",
"diagnosis_diskusage_low": "Der Speicher {mountpoint}
(auf Gerät {device}
) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von insgesamt {total}). Seien Sie vorsichtig.",
"diagnosis_ram_low": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total}). Seien Sie vorsichtig.",
- "service_reload_or_restart_failed": "Der Dienst '{service:s}' konnte nicht erneut geladen oder gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs:s}",
- "diagnosis_domain_expiration_not_found_details": "Die WHOIS-Informationen für die Domäne {domain} scheint keine Informationen über das Ablaufdatum zu enthalten.",
+ "service_reload_or_restart_failed": "Der Dienst '{service}' konnte nicht erneut geladen oder gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}",
+ "diagnosis_domain_expiration_not_found_details": "Die WHOIS-Informationen für die Domäne {domain} scheint keine Informationen über das Ablaufdatum zu enthalten?",
"diagnosis_domain_expiration_warning": "Einige Domänen werden bald ablaufen!",
"diagnosis_diskusage_ok": "Der Speicher {mountpoint}
(auf Gerät {device}
) hat immer noch {free} ({free_percent}%) freien Speicherplatz übrig(von insgesamt {total})!",
"diagnosis_ram_ok": "Das System hat immer noch {available} ({available_percent}%) RAM zu Verfügung von {total}.",
@@ -400,9 +359,9 @@
"diagnosis_mail_ehlo_unreachable_details": "Konnte keine Verbindung zu Ihrem Server auf dem Port 25 herzustellen per IPv{ipversion}. Er scheint nicht erreichbar zu sein.
1. Das häufigste Problem ist, dass der Port 25 nicht richtig zu Ihrem Server weitergeleitet ist.
2. Sie sollten auch sicherstellen, dass der Postfix-Dienst läuft.
3. In komplexeren Umgebungen: Stellen Sie sicher, daß keine Firewall oder Reverse-Proxy stört.",
"diagnosis_mail_ehlo_wrong": "Ein anderer SMTP-Server antwortet auf IPv{ipversion}. Ihr Server wird wahrscheinlich nicht in der Lage sein, E-Mails zu empfangen.",
"migration_description_0018_xtable_to_nftable": "Alte Netzwerkverkehrsregeln zum neuen nftable-System migrieren",
- "service_reload_failed": "Der Dienst '{service:s}' konnte nicht erneut geladen werden.\n\nKürzlich erstellte Logs des Dienstes: {logs:s}",
- "service_reloaded": "Der Dienst '{service:s}' wurde erneut geladen",
- "service_restart_failed": "Der Dienst '{service:s}' konnte nicht erneut gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs:s}",
+ "service_reload_failed": "Der Dienst '{service}' konnte nicht erneut geladen werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}",
+ "service_reloaded": "Der Dienst '{service}' wurde erneut geladen",
+ "service_restart_failed": "Der Dienst '{service}' konnte nicht erneut gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs}",
"app_manifest_install_ask_password": "Wählen Sie ein Verwaltungspasswort für diese Applikation",
"app_manifest_install_ask_domain": "Wählen Sie die Domäne, auf welcher die Applikation installiert werden soll",
"log_letsencrypt_cert_renew": "Erneuern des Let's Encrypt-Zeritifikates von '{}'",
@@ -416,7 +375,7 @@
"diagnosis_mail_ehlo_wrong_details": "Die vom Remote-Diagnose-Server per IPv{ipversion} empfangene EHLO weicht von der Domäne Ihres Servers ab.
Empfangene EHLO: {wrong_ehlo}
Erwartet: {right_ehlo}
Die geläufigste Ursache für dieses Problem ist, dass der Port 25 nicht korrekt auf Ihren Server weitergeleitet wird. Sie können sich zusätzlich auch versichern, dass keine Firewall oder Reverse-Proxy interferiert.",
"diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle Ihres Servers eine andere Maschine antwortet.",
"ask_user_domain": "Domäne, welche für die E-Mail-Adresse und den XMPP-Account des Benutzers verwendet werden soll",
- "app_manifest_install_ask_is_public": "Soll diese Applikation für anonyme Benutzer sichtbar sein?",
+ "app_manifest_install_ask_is_public": "Soll diese Applikation für anonyme Benutzer:innen sichtbar sein?",
"app_manifest_install_ask_admin": "Wählen Sie einen Administrator für diese Applikation",
"app_manifest_install_ask_path": "Wählen Sie den Pfad, in welchem die Applikation installiert werden soll",
"diagnosis_mail_blacklist_listed_by": "Ihre IP-Adresse oder Domäne {item}
ist auf der Blacklist auf {blacklist_name}",
@@ -424,12 +383,12 @@
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Aktueller Reverse-DNS-Eintrag: {rdns_domain}
Erwarteter Wert: {ehlo_domain}
",
"diagnosis_mail_fcrdns_different_from_ehlo_domain": "Der Reverse-DNS-Eintrag für IPv{ipversion} ist nicht korrekt konfiguriert. Einige E-Mails könnten abgewiesen oder als Spam markiert werden.",
"diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es Ihnen nicht erlauben, Ihren Reverse-DNS-Eintrag zu konfigurieren (oder ihre Funktionalität könnte defekt sein ...). Falls Ihr Reverse-DNS-Eintrag für IPv4 korrekt konfiguiert ist, können Sie versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem Sie den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführen. Bemerkung: Die Folge dieser letzten Lösung ist, dass Sie mit Servern, welche ausschliesslich über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen können.",
- "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es Ihnen nicht erlauben, dass Sie Ihren Reverse-DNS (oder deren Funktionalität ist defekt...) konfigurieren. Falls Sie deswegen auf Probleme stossen sollten, ziehen Sie folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schauen Sie hier nach https://yunohost.org/#/vpn_advantage
- Schliesslich ist es auch möglich zu einem anderen Anbieter zu wechseln",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es dir nicht erlauben, deinen Reverse-DNS zu konfigurieren (oder deren Funktionalität ist defekt...). Falls du deswegen auf Probleme stoßen solltest, ziehe folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schaue hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich zu einem anderen Anbieter zu wechseln",
"diagnosis_mail_queue_unavailable_details": "Fehler: {error}",
"diagnosis_mail_queue_unavailable": "Die Anzahl der anstehenden Nachrichten in der Warteschlange kann nicht abgefragt werden",
"diagnosis_mail_queue_ok": "{nb_pending} anstehende E-Mails in der Warteschlange",
"diagnosis_mail_blacklist_reason": "Der Grund für die Blacklist ist: {reason}",
- "app_argument_password_no_default": "Fehler beim Verarbeiten des Passwort-Arguments '{name}': Passwort-Argument kann aus Sicherheitsgründen keinen Standardwert haben",
+ "app_argument_password_no_default": "Fehler beim Verarbeiten des Passwortarguments '{name}': Passwortargument kann aus Sicherheitsgründen keinen Standardwert enthalten",
"log_regen_conf": "Systemkonfiguration neu generieren '{}'",
"diagnosis_http_partially_unreachable": "Die Domäne {domain} scheint von aussen via HTTP per IPv{failed} nicht erreichbar zu sein, obwohl es per IPv{passed} funktioniert.",
"diagnosis_http_unreachable": "Die Domäne {domain} scheint von aussen per HTTP nicht erreichbar zu sein.",
@@ -458,21 +417,21 @@
"diagnosis_mail_blacklist_website": "Nachdem Sie herausgefunden haben, weshalb Sie auf die Blacklist gesetzt wurden und dies behoben haben, zögern Sie nicht, nachzufragen, ob Ihre IP-Adresse oder Ihre Domäne von auf {blacklist_website} entfernt wird",
"diagnosis_unknown_categories": "Folgende Kategorien sind unbekannt: {categories}",
"diagnosis_http_hairpinning_issue": "In Ihrem lokalen Netzwerk scheint Hairpinning nicht aktiviert zu sein.",
- "diagnosis_ports_needed_by": "Diesen Port zu öffnen ist nötig, um die Funktionalität des Typs {category} (service {service}) zu gewährleisten.",
+ "diagnosis_ports_needed_by": "Diesen Port zu öffnen ist nötig, um die Funktionalität des Typs {category} (service {service}) zu gewährleisten",
"diagnosis_mail_queue_too_big": "Zu viele anstehende Nachrichten in der Warteschlange ({nb_pending} emails)",
- "diagnosis_package_installed_from_sury_details": "Einige Pakete wurden versehentlich aus einem Drittanbieter-Repository genannt Sury installiert. Das YunoHost-Team hat die Strategie um diese Pakete zu handhaben verbessert, aber es wird erwartet, dass einige Setups, welche PHP7.3-Applikationen installiert haben und immer noch auf Strech laufen ein paar Inkonsistenzen aufweisen. Um diese Situation zu beheben sollten Sie versuchen den folgenden Befehl auszuführen: {cmd_to_fix} ",
+ "diagnosis_package_installed_from_sury_details": "Einige Pakete wurden unbeabsichtigterweise aus einem Drittanbieter-Repository, genannt Sury, installiert. Das YunoHost-Team hat die Strategie, um diese Pakete zu handhaben, verbessert, aber es wird erwartet, dass einige Setups, welche PHP7.3-Applikationen installiert haben und immer noch auf Strech laufen, ein paar Inkonsistenzen aufweisen. Um diese Situation zu beheben, sollten Sie versuchen, den folgenden Befehl auszuführen: {cmd_to_fix} ",
"domain_cannot_add_xmpp_upload": "Eine hinzugefügte Domain darf nicht mit 'xmpp-upload.' beginnen. Dieser Name ist für das XMPP-Upload-Feature von YunoHost reserviert.",
"group_cannot_be_deleted": "Die Gruppe {group} kann nicht manuell entfernt werden.",
"group_cannot_edit_primary_group": "Die Gruppe '{group}' kann nicht manuell bearbeitet werden. Es ist die primäre Gruppe, welche dazu gedacht ist, nur einen spezifischen Benutzer zu enthalten.",
"diagnosis_processes_killed_by_oom_reaper": "Das System hat einige Prozesse beendet, weil ihm der Arbeitsspeicher ausgegangen ist. Das passiert normalerweise, wenn das System ingesamt nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein einzelner Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: \n{kills_summary}",
"diagnosis_description_ports": "Offene Ports",
- "additional_urls_already_added": "Zusätzliche URL '{url:s}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission:s}'",
- "additional_urls_already_removed": "Zusätzliche URL '{url:s}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission:s}'",
+ "additional_urls_already_added": "Zusätzliche URL '{url}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission}'",
+ "additional_urls_already_removed": "Zusätzliche URL '{url}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission}'",
"app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutzen Sie den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.",
- "diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in https://yunohost.org/dns_local_network werfen",
- "diagnosis_http_nginx_conf_not_up_to_date": "Jemand hat anscheinend die Konfiguration von Nginx manuell geändert. Diese Änderung verhindert, dass Yunohost eine Diagnose durchführen kann, wenn er via HTTP erreichbar ist.",
+ "diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in https://yunohost.org/dns_local_network werfen",
+ "diagnosis_http_nginx_conf_not_up_to_date": "Jemand hat anscheinend die Konfiguration von Nginx manuell geändert. Diese Änderung verhindert, dass YunoHost eine Diagnose durchführen kann, wenn er via HTTP erreichbar ist.",
"diagnosis_http_bad_status_code": "Anscheinend beantwortet ein anderes Gerät als Ihr Server die Anfrage (Vielleicht ihr Internetrouter).
1. Die häufigste Ursache ist, dass Port 80 (und 443) nicht richtig auf Ihren Server weitergeleitet wird.
2. Bei komplexeren Setups: Vergewissern Sie sich, dass keine Firewall und keine Reverse-Proxy interferieren.",
- "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'.",
+ "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen YunoHost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'.",
"diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.",
"diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.",
"diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}",
@@ -480,8 +439,8 @@
"group_user_not_in_group": "Der Benutzer {user} ist nicht in der Gruppe {group}",
"group_user_already_in_group": "Der Benutzer {user} ist bereits in der Gruppe {group}",
"group_cannot_edit_visitors": "Die Gruppe \"Besucher\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe und repräsentiert anonyme Besucher",
- "group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Benutzer in Yunohost zu halten",
- "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber Yunohost wird sie entfernen...",
+ "group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Benutzer in YunoHost zu halten",
+ "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber YunoHost wird sie entfernen...",
"group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen",
"group_already_exist": "Die Gruppe {group} existiert bereits",
"global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort",
@@ -489,5 +448,187 @@
"global_settings_setting_smtp_relay_port": "SMTP Relay Port",
"global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden",
"global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver",
- "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domain:s}\" entfernen."
-}
+ "domain_cannot_remove_main_add_new_one": "Sie können '{domain}' nicht entfernen, weil es die Hauptdomäne und gleichzeitig Ihre einzige Domäne ist. Zuerst müssen Sie eine andere Domäne hinzufügen, indem Sie \"yunohost domain add another-domain.com>\" eingeben. Bestimmen Sie diese dann als Ihre Hauptdomain indem Sie 'yunohost domain main-domain -n ' eingeben. Nun können Sie die Domäne \"{domain}\" enfernen, indem Sie 'yunohost domain remove {domain}' eingeben.'",
+ "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.",
+ "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.",
+ "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn Sie in einer der folgenden Situationen sind: Ihr ISP- oder VPS-Provider hat Ihren Port 25 geblockt, eine Ihrer residentiellen IPs ist auf DUHL gelistet, Sie können keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und Sie möchten einen anderen verwenden, um E-Mails zu versenden.",
+ "global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.",
+ "log_remove_on_failed_restore": "'{}' entfernen nach einer fehlerhaften Wiederherstellung aus einem Backup-Archiv",
+ "log_backup_restore_app": "'{}' aus einem Backup-Archiv wiederherstellen",
+ "log_backup_restore_system": "System aus einem Backup-Archiv wiederherstellen",
+ "log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar",
+ "log_app_action_run": "Führe Aktion der Applikation '{}' aus",
+ "invalid_regex": "Ungültige Regex:'{regex}'",
+ "migration_description_0016_php70_to_php73_pools": "Migrieren der php7.0-fpm-Konfigurationsdateien zu php7.3",
+ "mailbox_disabled": "E-Mail für Benutzer {user} deaktiviert",
+ "log_tools_reboot": "Server neustarten",
+ "log_tools_shutdown": "Server ausschalten",
+ "log_tools_upgrade": "Systempakete aktualisieren",
+ "log_tools_postinstall": "Post-Installation des YunoHost-Servers durchführen",
+ "log_tools_migrations_migrate_forward": "Migrationen durchführen",
+ "log_domain_main_domain": "Mache '{}' zur Hauptdomäne",
+ "log_user_permission_reset": "Zurücksetzen der Berechtigung '{}'",
+ "log_user_permission_update": "Aktualisiere Zugriffe für Berechtigung '{}'",
+ "log_user_update": "Aktualisiere Information für Benutzer '{}'",
+ "log_user_group_update": "Aktualisiere Gruppe '{}'",
+ "log_user_group_delete": "Lösche Gruppe '{}'",
+ "log_user_group_create": "Erstelle Gruppe '{}'",
+ "log_user_delete": "Lösche Benutzer '{}'",
+ "log_user_create": "Füge Benutzer '{}' hinzu",
+ "log_permission_url": "Aktualisiere URL, die mit der Berechtigung '{}' verknüpft ist",
+ "log_permission_delete": "Lösche Berechtigung '{}'",
+ "log_permission_create": "Erstelle Berechtigung '{}'",
+ "log_dyndns_update": "Die IP, die mit der YunoHost-Subdomain '{}' verbunden ist, aktualisieren",
+ "log_dyndns_subscribe": "Für eine YunoHost-Subdomain registrieren '{}'",
+ "log_domain_remove": "Entfernen der Domäne '{}' aus der Systemkonfiguration",
+ "log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration",
+ "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation",
+ "migration_0015_still_on_stretch_after_main_upgrade": "Etwas ist schiefgelaufen während dem Haupt-Upgrade. Das System scheint immer noch auf Debian Stretch zu laufen",
+ "migration_0015_yunohost_upgrade": "Beginne YunoHost-Core-Upgrade...",
+ "migration_description_0019_extend_permissions_features": "Erweitern und überarbeiten des Applikationsberechtigungs-Managementsystems",
+ "migrating_legacy_permission_settings": "Migrieren der Legacy-Berechtigungseinstellungen...",
+ "migration_description_0017_postgresql_9p6_to_11": "Migrieren der Datenbanken von PostgreSQL 9.6 nach 11",
+ "migration_0015_main_upgrade": "Beginne Haupt-Upgrade...",
+ "migration_0015_not_stretch": "Die aktuelle Debian-Distribution ist nicht Stretch!",
+ "migration_0015_not_enough_free_space": "Der freie Speicher in /var/ ist sehr gering! Sie sollten minimal 1GB frei haben, um diese Migration durchzuführen.",
+ "domain_remove_confirm_apps_removal": "Wenn Sie diese Domäne löschen, werden folgende Applikationen entfernt:\n{apps}\n\nSind Sie sicher? [{answers}]",
+ "migration_0015_cleaning_up": "Bereinigung des Cache und der Pakete, welche nicht mehr benötigt werden...",
+ "migration_0017_postgresql_96_not_installed": "PostgreSQL wurde auf ihrem System nicht installiert. Nichts zu tun.",
+ "migration_0015_system_not_fully_up_to_date": "Ihr System ist nicht vollständig auf dem neuesten Stand. Bitte führen Sie ein reguläres Upgrade durch, bevor Sie die Migration auf Buster durchführen.",
+ "migration_0015_modified_files": "Bitte beachten Sie, dass die folgenden Dateien als manuell bearbeitet erkannt wurden und beim nächsten Upgrade überschrieben werden könnten: {manually_modified_files}",
+ "migration_0015_general_warning": "Bitte beachten Sie, dass diese Migration eine heikle Angelegenheit darstellt. Das YunoHost-Team hat alles unternommen, um sie zu testen und zu überarbeiten. Dennoch ist es möglich, dass diese Migration Teile des Systems oder Applikationen beschädigen könnte.\n\nDeshalb ist folgendes zu empfehlen:\n…- Führen Sie ein Backup aller kritischen Daten und Applikationen durch. Mehr unter https://yunohost.org/backup;\n…- Seien Sie geduldig nachdem Sie die Migration gestartet haben: Abhängig von Ihrer Internetverbindung und Ihrer Hardware kann es einige Stunden dauern, bis das Upgrade fertig ist.",
+ "migration_0015_problematic_apps_warning": "Bitte beachten Sie, dass folgende möglicherweise problematischen Applikationen auf Ihrer Installation erkannt wurden. Es scheint, als ob sie nicht aus dem YunoHost-Applikationskatalog installiert oder nicht als 'working' gekennzeichnet worden sind. Folglich kann nicht garantiert werden, dass sie nach dem Upgrade immer noch funktionieren: {problematic_apps}",
+ "migration_0015_specific_upgrade": "Start des Upgrades der Systempakete, deren Upgrade separat durchgeführt werden muss...",
+ "migration_0015_weak_certs": "Die folgenden Zertifikate verwenden immer noch schwache Signierungsalgorithmen und müssen aktualisiert werden um mit der nächsten Version von nginx kompatibel zu sein: {certs}",
+ "migrations_pending_cant_rerun": "Diese Migrationen sind immer noch anstehend und können deshalb nicht erneut durchgeführt werden: {ids}",
+ "migration_0019_add_new_attributes_in_ldap": "Hinzufügen neuer Attribute für die Berechtigungen in der LDAP-Datenbank",
+ "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht anstehend und können deshalb nicht übersprungen werden: {ids}",
+ "migration_0018_failed_to_reset_legacy_rules": "Zurücksetzen der veralteten iptables-Regeln fehlgeschlagen: {error}",
+ "migration_0019_slapd_config_will_be_overwritten": "Es schaut aus, als ob Sie die slapd-Konfigurationsdatei manuell bearbeitet haben. Für diese kritische Migration muss das Update der slapd-Konfiguration erzwungen werden. Von der Originaldatei wird ein Backup gemacht in {conf_backup_folder}.",
+ "migrations_success_forward": "Migration {id} abgeschlossen",
+ "migrations_cant_reach_migration_file": "Die Migrationsdateien konnten nicht aufgerufen werden im Verzeichnis '%s'",
+ "migrations_dependencies_not_satisfied": "Führen Sie diese Migrationen aus: '{dependencies_id}', vor der Migration {id}.",
+ "migrations_failed_to_load_migration": "Konnte Migration nicht laden {id}: {error}",
+ "migrations_list_conflict_pending_done": "Sie können nicht '--previous' und '--done' gleichzeitig benützen.",
+ "migrations_already_ran": "Diese Migrationen wurden bereits durchgeführt: {ids}",
+ "migrations_loading_migration": "Lade Migrationen {id}...",
+ "migrations_migration_has_failed": "Migration {id} gescheitert mit der Ausnahme {exception}: Abbruch",
+ "migrations_must_provide_explicit_targets": "Sie müssen konkrete Ziele angeben, wenn Sie '--skip' oder '--force-rerun' verwenden",
+ "migrations_need_to_accept_disclaimer": "Um die Migration {id} durchzuführen, müssen Sie den Disclaimer akzeptieren.\n---\n{disclaimer}\n---\n Wenn Sie bestätigen, dass Sie die Migration durchführen wollen, wiederholen Sie bitte den Befehl mit der Option '--accept-disclaimer'.",
+ "migrations_no_migrations_to_run": "Keine Migrationen durchzuführen",
+ "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 ist installiert aber nicht postgreSQL 11? Etwas komisches ist Ihrem System zugestossen :(...",
+ "migration_0017_not_enough_space": "Stellen Siea ausreichend Speicherplatz im Verzeichnis {path} zur Verfügung um die Migration durchzuführen.",
+ "migration_0018_failed_to_migrate_iptables_rules": "Migration der veralteten iptables-Regeln zu nftables fehlgeschlagen: {error}",
+ "migrations_exclusive_options": "'--auto', '--skip' und '--force-rerun' sind Optionen, die sich gegenseitig ausschliessen.",
+ "migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'",
+ "migrations_running_forward": "Durchführen der Migrationen {id}...",
+ "migrations_skip_migration": "Überspringe Migrationen {id}...",
+ "password_too_simple_2": "Das Passwort muss mindestens 8 Zeichen lang sein und Gross- sowie Kleinbuchstaben enthalten",
+ "password_listed": "Dieses Passwort zählt zu den meistgenutzten Passwörtern der Welt. Bitte wähle ein anderes, einzigartigeres Passwort.",
+ "operation_interrupted": "Wurde die Operation manuell unterbrochen?",
+ "invalid_number": "Muss eine Zahl sein",
+ "migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehen Sie zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führen Sie 'yunohost tools migrations run' aus.",
+ "permission_already_up_to_date": "Die Berechtigung wurde nicht aktualisiert, weil die Anfragen für Hinzufügen/Entfernen bereits mit dem aktuellen Status übereinstimmen.",
+ "permission_already_exist": "Berechtigung '{permission}' existiert bereits",
+ "permission_already_disallowed": "Für die Gruppe '{group}' wurde die Berechtigung '{permission}' deaktiviert",
+ "permission_already_allowed": "Die Gruppe '{group}' hat die Berechtigung '{permission}' bereits erhalten",
+ "pattern_password_app": "Entschuldigen Sie bitte! Passwörter dürfen folgende Zeichen nicht enthalten: {forbidden_chars}",
+ "pattern_email_forward": "Es muss sich um eine gültige E-Mail-Adresse handeln. Das Symbol '+' wird akzeptiert (zum Beispiel : maxmuster@beispiel.com oder maxmuster+yunohost@beispiel.com)",
+ "password_too_simple_4": "Das Passwort muss mindestens 12 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten",
+ "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten",
+ "regenconf_file_manually_removed": "Die Konfigurationsdatei '{conf}' wurde manuell gelöscht und wird nicht erstellt",
+ "regenconf_file_manually_modified": "Die Konfigurationsdatei '{conf}' wurde manuell bearbeitet und wird nicht aktualisiert",
+ "regenconf_file_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {category}) gelöscht werden, wurde aber beibehalten.",
+ "regenconf_file_copy_failed": "Die neue Konfigurationsdatei '{new}' kann nicht nach '{conf}' kopiert werden",
+ "regenconf_file_backed_up": "Die Konfigurationsdatei '{conf}' wurde unter '{backup}' gespeichert",
+ "permission_require_account": "Berechtigung {permission} ist nur für Benutzer mit einem Konto sinnvoll und kann daher nicht für Besucher aktiviert werden.",
+ "permission_protected": "Die Berechtigung ist geschützt. Sie können die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.",
+ "permission_updated": "Berechtigung '{permission}' aktualisiert",
+ "permission_update_failed": "Die Berechtigung '{permission}' kann nicht aktualisiert werden : {error}",
+ "permission_not_found": "Berechtigung '{permission}' nicht gefunden",
+ "permission_deletion_failed": "Entfernung der Berechtigung nicht möglich '{permission}': {error}",
+ "permission_deleted": "Berechtigung '{permission}' gelöscht",
+ "permission_currently_allowed_for_all_users": "Diese Berechtigung wird derzeit allen Benutzern zusätzlich zu anderen Gruppen erteilt. Möglicherweise möchten Sie entweder die Berechtigung 'all_users' entfernen oder die anderen Gruppen entfernen, für die sie derzeit zulässig sind.",
+ "permission_creation_failed": "Berechtigungserstellung nicht möglich '{permission}' : {error}",
+ "permission_created": "Berechtigung '{permission}' erstellt",
+ "permission_cannot_remove_main": "Entfernung einer Hauptberechtigung nicht genehmigt",
+ "regenconf_file_updated": "Konfigurationsdatei '{conf}' aktualisiert",
+ "regenconf_file_removed": "Konfigurationsdatei '{conf}' entfernt",
+ "regenconf_file_remove_failed": "Konnte die Konfigurationsdatei '{conf}' nicht entfernen",
+ "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace",
+ "regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'",
+ "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet.",
+ "regenconf_updated": "Konfiguration aktualisiert für '{category}'",
+ "regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an...",
+ "regenconf_failed": "Konnte die Konfiguration für die Kategorie(n) {categories} nicht neu erstellen",
+ "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre...",
+ "regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden",
+ "restore_system_part_failed": "Die Systemteile '{part}' konnten nicht wiederhergestellt werden",
+ "restore_removing_tmp_dir_failed": "Ein altes, temporäres Directory konnte nicht entfernt werden",
+ "restore_not_enough_disk_space": "Nicht genug Speicher (Speicher: {free_space} B, benötigter Speicher: {needed_space} B, Sicherheitspuffer: {margin} B)",
+ "restore_may_be_not_enough_disk_space": "Ihr System scheint nicht genug Speicherplatz zu haben (frei: {free_space} B, benötigter Platz: {needed_space} B, Sicherheitspuffer: {margin} B)",
+ "restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus...",
+ "restore_already_installed_apps": "Folgende Apps können nicht wiederhergestellt werden, weil sie schon installiert sind: {apps}",
+ "regex_with_only_domain": "Du kannst regex nicht als Domain verwenden, sondern nur als Pfad",
+ "root_password_desynchronized": "Das Admin-Passwort wurde verändert, aber das Root-Passwort ist immer noch das alte!",
+ "regenconf_need_to_explicitly_specify_ssh": "Die SSH-Konfiguration wurde manuell modifiziert, aber Sie müssen explizit die Kategorie 'SSH' mit --force spezifizieren, um die Änderungen tatsächlich anzuwenden.",
+ "migration_update_LDAP_schema": "Aktualisiere das LDAP-Schema...",
+ "log_backup_create": "Erstelle ein Backup-Archiv",
+ "diagnosis_sshd_config_inconsistent": "Es sieht aus, als ob der SSH-Port manuell geändert wurde in /etc/ssh/ssh_config. Seit YunoHost 4.2 ist eine neue globale Einstellung 'security.ssh.port' verfügbar um zu verhindern, dass die Konfiguration manuell verändert wird.",
+ "diagnosis_sshd_config_insecure": "Die SSH-Konfiguration wurde scheinbar manuell geändert und ist unsicher, weil sie keine 'AllowGroups'- oder 'AllowUsers' -Direktiven für die Beschränkung des Zugriffs durch autorisierte Benutzer enthält.",
+ "backup_create_size_estimation": "Das Archiv wird etwa {size} an Daten enthalten.",
+ "app_restore_script_failed": "Im Wiederherstellungsskript der Applikation ist ein Fehler aufgetreten",
+ "app_restore_failed": "Konnte {app} nicht wiederherstellen: {error}",
+ "migration_ldap_rollback_success": "System-Rollback erfolgreich.",
+ "migration_ldap_migration_failed_trying_to_rollback": "Migrieren war nicht möglich... Versuch, ein Rollback des Systems durchzuführen.",
+ "migration_ldap_backup_before_migration": "Vor der eigentlichen Migration ein Backup der LDAP-Datenbank und der Applikations-Einstellungen erstellen.",
+ "migration_description_0020_ssh_sftp_permissions": "Unterstützung für SSH- und SFTP-Berechtigungen hinzufügen",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "Das SSOwat-Overlay-Panel aktivieren",
+ "global_settings_setting_security_ssh_port": "SSH-Port",
+ "diagnosis_sshd_config_inconsistent_details": "Bitte führen Sie yunohost settings set security.ssh.port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und prüfen Sie yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um Ihre conf auf die YunoHost-Empfehlung zurückzusetzen.",
+ "regex_incompatible_with_tile": "/!\\ Packagers! Für Berechtigung '{permission}' ist show_tile auf 'true' gesetzt und deshalb können Sie keine regex-URL als Hauptdomäne setzen",
+ "permission_cant_add_to_all_users": "Die Berechtigung {permission} konnte nicht allen Benutzern gegeben werden.",
+ "migration_ldap_can_not_backup_before_migration": "Das System-Backup konnte nicht abgeschlossen werden, bevor die Migration fehlschlug. Fehler: {error}",
+ "service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet",
+ "service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)",
+ "service_description_dnsmasq": "Verarbeitet die Auflösung des Domainnamens (DNS)",
+ "restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.",
+ "service_description_slapd": "Speichert Benutzer, Domains und verbundene Informationen",
+ "service_description_rspamd": "Spamfilter und andere E-Mail-Merkmale",
+ "service_description_redis-server": "Eine spezialisierte Datenbank für den schnellen Datenzugriff, die Aufgabenwarteschlange und die Kommunikation zwischen Programmen",
+ "service_description_postfix": "Wird benutzt, um E-Mails zu senden und zu empfangen",
+ "service_description_nginx": "Stellt Daten aller Websiten auf dem Server bereit",
+ "service_description_mysql": "Speichert die Applikationsdaten (SQL Datenbank)",
+ "service_description_metronome": "XMPP Sofortnachrichtenkonten verwalten",
+ "service_description_yunohost-firewall": "Verwaltet offene und geschlossene Ports zur Verbindung mit Diensten",
+ "service_description_yunohost-api": "Verwaltet die Interaktionen zwischen der Weboberfläche von YunoHost und dem System",
+ "service_description_ssh": "Ermöglicht die Verbindung zu Ihrem Server über ein Terminal (SSH-Protokoll)",
+ "service_description_php7.3-fpm": "Führt in PHP geschriebene Apps mit NGINX aus",
+ "server_reboot_confirm": "Der Server wird sofort heruntergefahren, sind Sie sicher? [{answers}]",
+ "server_reboot": "Der Server wird neu gestartet",
+ "server_shutdown_confirm": "Der Server wird sofort heruntergefahren, sind Sie sicher? [{answers}]",
+ "server_shutdown": "Der Server wird heruntergefahren",
+ "root_password_replaced_by_admin_password": "Ihr Root Passwort wurde durch Ihr Admin Passwort ersetzt.",
+ "show_tile_cant_be_enabled_for_regex": "Momentan können Sie 'show_tile' nicht aktivieren, weil die URL für die Berechtigung '{permission}' ein regulärer Ausdruck ist",
+ "show_tile_cant_be_enabled_for_url_not_defined": "Momentan können Sie 'show_tile' nicht aktivieren, weil Sie zuerst eine URL für die Berechtigung '{permission}' definieren müssen",
+ "tools_upgrade_regular_packages_failed": "Konnte für die folgenden Pakete das Upgrade nicht durchführen: {packages_list}",
+ "tools_upgrade_regular_packages": "Momentan werden Upgrades für das System (YunoHost-unabhängige) Pakete durchgeführt...",
+ "tools_upgrade_cant_unhold_critical_packages": "Konnte für die kritischen Pakete das Flag 'hold' nicht aufheben...",
+ "tools_upgrade_cant_hold_critical_packages": "Konnte für die kritischen Pakete das Flag 'hold' nicht setzen...",
+ "tools_upgrade_cant_both": "Kann das Upgrade für das System und die Applikation nicht gleichzeitig durchführen",
+ "tools_upgrade_at_least_one": "Bitte geben Sie '--apps' oder '--system' an",
+ "this_action_broke_dpkg": "Diese Aktion hat unkonfigurierte Pakete verursacht, welche durch dpkg/apt (die Paketverwaltungen dieses Systems) zurückgelassen wurden... Sie können versuchen dieses Problem zu lösen, indem Sie 'sudo apt install --fix-broken' und/oder 'sudo dpkg --configure -a' ausführen.",
+ "update_apt_cache_failed": "Kann den Cache von APT (Debians Paketmanager) nicht aktualisieren. Hier ist ein Auszug aus den sources.list-Zeilen, die helfen könnten, das Problem zu identifizieren:\n{sourceslist}",
+ "tools_upgrade_special_packages_completed": "YunoHost-Paketupdate beendet.\nDrücke [Enter], um zurück zur Kommandoziele zu kommen",
+ "tools_upgrade_special_packages_explanation": "Das Upgrade \"special\" wird im Hintergrund ausgeführt. Bitte starten Sie keine anderen Aktionen auf Ihrem Server für die nächsten ~10 Minuten. Die Dauer ist abhängig von der Geschwindigkeit Ihres Servers. Nach dem Upgrade müssen Sie sich eventuell erneut in das Adminportal einloggen. Upgrade-Logs sind im Adminbereich unter Tools → Log verfügbar. Alternativ können Sie in der Befehlszeile 'yunohost log list' eingeben.",
+ "tools_upgrade_special_packages": "\"special\" (YunoHost-bezogene) Pakete werden jetzt aktualisiert...",
+ "unknown_main_domain_path": "Unbekannte:r Domain oder Pfad für '{app}'. Du musst eine Domain und einen Pfad setzen, um die URL für Berechtigungen zu setzen.",
+ "yunohost_postinstall_end_tip": "Post-install ist fertig! Um das Setup abzuschliessen, wird empfohlen:\n - einen ersten Benutzer über den Bereich 'Benutzer*in' im Adminbereich hinzuzufügen (oder mit 'yunohost user create ' in der Kommandezeile);\n - mögliche Fehler zu diagnostizieren über den Bereich 'Diagnose' im Adminbereich (oder mit 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch zu lesen: https://yunohost.org/admindoc.",
+ "user_already_exists": "Der Benutzer '{user}' ist bereits vorhanden",
+ "update_apt_cache_warning": "Beim Versuch den Cache für APT (Debians Paketmanager) zu aktualisieren, ist etwas schief gelaufen. Hier ist ein Dump der Zeilen aus sources.list, die Ihnen vielleicht dabei helfen, das Problem zu identifizieren:\n{sourceslist}",
+ "global_settings_setting_security_webadmin_allowlist": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.",
+ "global_settings_setting_security_webadmin_allowlist_enabled": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.",
+ "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktuallisieren",
+ "disk_space_not_sufficient_install": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu installieren",
+ "danger": "Warnung:"
+}
\ No newline at end of file
diff --git a/locales/el.json b/locales/el.json
index b43f11d5d..a85bd0710 100644
--- a/locales/el.json
+++ b/locales/el.json
@@ -1,3 +1,4 @@
{
- "password_too_simple_1": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 8 χαρακτήρες"
-}
+ "password_too_simple_1": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 8 χαρακτήρες",
+ "aborting": "Ματαίωση."
+}
\ No newline at end of file
diff --git a/locales/en.json b/locales/en.json
index 88b3d6a9b..e2ad871e9 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1,70 +1,71 @@
{
"aborting": "Aborting.",
- "action_invalid": "Invalid action '{action:s}'",
- "additional_urls_already_added": "Additionnal URL '{url:s}' already added in the additional URL for permission '{permission:s}'",
- "additional_urls_already_removed": "Additionnal URL '{url:s}' already removed in the additional URL for permission '{permission:s}'",
+ "action_invalid": "Invalid action '{action}'",
+ "additional_urls_already_added": "Additionnal URL '{url}' already added in the additional URL for permission '{permission}'",
+ "additional_urls_already_removed": "Additionnal URL '{url}' already removed in the additional URL for permission '{permission}'",
"admin_password": "Administration password",
"admin_password_change_failed": "Unable to change password",
"admin_password_changed": "The administration password was changed",
"admin_password_too_long": "Please choose a password shorter than 127 characters",
"already_up_to_date": "Nothing to do. Everything is already up-to-date.",
- "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).",
"app_action_broke_system": "This action seems to have broken these important services: {services}",
- "app_already_installed": "{app:s} is already installed",
+ "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).",
+ "app_already_installed": "{app} is already installed",
"app_already_installed_cant_change_url": "This app is already installed. The URL cannot be changed just by this function. Check in `app changeurl` if it's available.",
- "app_already_up_to_date": "{app:s} is already up-to-date",
- "app_argument_choice_invalid": "Use one of these choices '{choices:s}' for the argument '{name:s}'",
- "app_argument_invalid": "Pick a valid value for the argument '{name:s}': {error:s}",
+ "app_already_up_to_date": "{app} is already up-to-date",
+ "app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})",
+ "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}",
"app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reason",
- "app_argument_required": "Argument '{name:s}' is required",
- "app_change_url_failed_nginx_reload": "Could not reload NGINX. Here is the output of 'nginx -t':\n{nginx_errors:s}",
- "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.",
- "app_change_url_no_script": "The app '{app_name:s}' doesn't support URL modification yet. Maybe you should upgrade it.",
- "app_change_url_success": "{app:s} URL is now {domain:s}{path:s}",
+ "app_argument_required": "Argument '{name}' is required",
+ "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.",
+ "app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.",
+ "app_change_url_success": "{app} URL is now {domain}{path}",
+ "app_config_unable_to_apply": "Failed to apply config panel values.",
+ "app_config_unable_to_read": "Failed to read config panel values.",
"app_extraction_failed": "Could not extract the installation files",
"app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.",
"app_id_invalid": "Invalid app ID",
- "app_install_files_invalid": "These files cannot be installed",
"app_install_failed": "Unable to install {app}: {error}",
+ "app_install_files_invalid": "These files cannot be installed",
"app_install_script_failed": "An error occurred inside the app installation script",
- "app_make_default_location_already_used": "Unable to make '{app}' the default app on the domain, '{domain}' is already in use by '{other_app}'",
"app_label_deprecated": "This command is deprecated! Please use the new command 'yunohost user permission update' to manage the app label.",
- "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps:s}",
- "app_manifest_invalid": "Something is wrong with the app manifest: {error}",
- "app_manifest_install_ask_domain": "Choose the domain where this app should be installed",
- "app_manifest_install_ask_path": "Choose the path where this app should be installed",
- "app_manifest_install_ask_password": "Choose an administration password for this app",
+ "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps}",
+ "app_make_default_location_already_used": "Unable to make '{app}' the default app on the domain, '{domain}' is already in use by '{other_app}'",
"app_manifest_install_ask_admin": "Choose an administrator user for this app",
+ "app_manifest_install_ask_domain": "Choose the domain where this app should be installed",
"app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?",
+ "app_manifest_install_ask_password": "Choose an administration password for this app",
+ "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed",
+ "app_not_correctly_installed": "{app} seems to be incorrectly installed",
+ "app_not_installed": "Could not find {app} in the list of installed apps: {all_apps}",
+ "app_not_properly_removed": "{app} has not been properly removed",
"app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}",
- "app_not_correctly_installed": "{app:s} seems to be incorrectly installed",
- "app_not_installed": "Could not find {app:s} in the list of installed apps: {all_apps}",
- "app_not_properly_removed": "{app:s} has not been properly removed",
- "app_removed": "{app:s} removed",
+ "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.",
+ "app_remove_after_failed_install": "Removing the app following the installation failure...",
+ "app_removed": "{app} uninstalled",
"app_requirements_checking": "Checking required packages for {app}...",
"app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}",
- "app_remove_after_failed_install": "Removing the app following the installation failure...",
+ "app_restore_failed": "Could not restore {app}: {error}",
+ "app_restore_script_failed": "An error occured inside the app restore script",
"app_sources_fetch_failed": "Could not fetch sources files, is the URL correct?",
+ "app_start_backup": "Collecting files to be backed up for {app}...",
"app_start_install": "Installing {app}...",
"app_start_remove": "Removing {app}...",
- "app_start_backup": "Collecting files to be backed up for {app}...",
"app_start_restore": "Restoring {app}...",
"app_unknown": "Unknown app",
"app_unsupported_remote_type": "Unsupported remote type used for the app",
- "app_upgrade_several_apps": "The following apps will be upgraded: {apps}",
"app_upgrade_app_name": "Now upgrading {app}...",
- "app_upgrade_failed": "Could not upgrade {app:s}: {error}",
+ "app_upgrade_failed": "Could not upgrade {app}: {error}",
"app_upgrade_script_failed": "An error occurred inside the app upgrade script",
+ "app_upgrade_several_apps": "The following apps will be upgraded: {apps}",
"app_upgrade_some_app_failed": "Some apps could not be upgraded",
- "app_upgraded": "{app:s} upgraded",
- "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.",
+ "app_upgraded": "{app} upgraded",
"apps_already_up_to_date": "All apps are already up-to-date",
- "apps_catalog_init_success": "App catalog system initialized!",
- "apps_catalog_updating": "Updating application catalog…",
"apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}",
+ "apps_catalog_init_success": "App catalog system initialized!",
"apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.",
"apps_catalog_update_success": "The application catalog has been updated!",
- "ask_user_domain": "Domain to use for the user's email address and XMPP account",
+ "apps_catalog_updating": "Updating application catalog...",
"ask_firstname": "First name",
"ask_lastname": "Last name",
"ask_main_domain": "Main domain",
@@ -72,37 +73,39 @@
"ask_new_domain": "New domain",
"ask_new_path": "New path",
"ask_password": "Password",
+ "ask_user_domain": "Domain to use for the user's email address and XMPP account",
"backup_abstract_method": "This backup method has yet to be implemented",
"backup_actually_backuping": "Creating a backup archive from the collected files...",
- "backup_app_failed": "Could not back up {app:s}",
+ "backup_app_failed": "Could not back up {app}",
"backup_applying_method_copy": "Copying all files to backup...",
- "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...",
+ "backup_applying_method_custom": "Calling the custom backup method '{method}'...",
"backup_applying_method_tar": "Creating the backup TAR archive...",
- "backup_archive_app_not_found": "Could not find {app:s} in the backup archive",
- "backup_archive_broken_link": "Could not access the backup archive (broken link to {path:s})",
- "backup_archive_name_exists": "A backup archive with this name already exists.",
- "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'",
- "backup_archive_open_failed": "Could not open the backup archive",
+ "backup_archive_app_not_found": "Could not find {app} in the backup archive",
+ "backup_archive_broken_link": "Could not access the backup archive (broken link to {path})",
"backup_archive_cant_retrieve_info_json": "Could not load infos for archive '{archive}'... The info.json cannot be retrieved (or is not a valid json).",
"backup_archive_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}",
- "backup_archive_system_part_not_available": "System part '{part:s}' unavailable in this backup",
- "backup_archive_writing_error": "Could not add the files '{source:s}' (named in the archive '{dest:s}') to be backed up into the compressed archive '{archive:s}'",
- "backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size:s}MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)",
+ "backup_archive_name_exists": "A backup archive with this name already exists.",
+ "backup_archive_name_unknown": "Unknown local backup archive named '{name}'",
+ "backup_archive_open_failed": "Could not open the backup archive",
+ "backup_archive_system_part_not_available": "System part '{part}' unavailable in this backup",
+ "backup_archive_writing_error": "Could not add the files '{source}' (named in the archive '{dest}') to be backed up into the compressed archive '{archive}'",
+ "backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size}MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)",
"backup_cant_mount_uncompress_archive": "Could not mount the uncompressed archive as write protected",
"backup_cleaning_failed": "Could not clean up the temporary backup folder",
- "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive",
- "backup_couldnt_bind": "Could not bind {src:s} to {dest:s}.",
+ "backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive",
+ "backup_couldnt_bind": "Could not bind {src} to {dest}.",
+ "backup_create_size_estimation": "The archive will contain about {size} of data.",
"backup_created": "Backup created",
"backup_creation_failed": "Could not create the backup archive",
"backup_csv_addition_failed": "Could not add files to backup into the CSV file",
"backup_csv_creation_failed": "Could not create the CSV file needed for restoration",
"backup_custom_backup_error": "Custom backup method could not get past the 'backup' step",
"backup_custom_mount_error": "Custom backup method could not get past the 'mount' step",
- "backup_delete_error": "Could not delete '{path:s}'",
+ "backup_delete_error": "Could not delete '{path}'",
"backup_deleted": "Backup deleted",
- "backup_hook_unknown": "The backup hook '{hook:s}' is unknown",
+ "backup_hook_unknown": "The backup hook '{hook}' is unknown",
"backup_method_copy_finished": "Backup copy finalized",
- "backup_method_custom_finished": "Custom backup method '{method:s}' finished",
+ "backup_method_custom_finished": "Custom backup method '{method}' finished",
"backup_method_tar_finished": "TAR backup archive created",
"backup_mount_archive_for_restore": "Preparing archive for restoration...",
"backup_no_uncompress_archive_dir": "There is no such uncompressed archive directory",
@@ -110,190 +113,245 @@
"backup_output_directory_forbidden": "Pick a different output directory. Backups cannot be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders",
"backup_output_directory_not_empty": "You should pick an empty output directory",
"backup_output_directory_required": "You must provide an output directory for the backup",
- "backup_output_symlink_dir_broken": "Your archive directory '{path:s}' is a broken symlink. Maybe you forgot to re/mount or plug in the storage medium it points to.",
- "backup_permission": "Backup permission for {app:s}",
+ "backup_output_symlink_dir_broken": "Your archive directory '{path}' is a broken symlink. Maybe you forgot to re/mount or plug in the storage medium it points to.",
+ "backup_permission": "Backup permission for {app}",
"backup_running_hooks": "Running backup hooks...",
- "backup_system_part_failed": "Could not backup the '{part:s}' system part",
+ "backup_system_part_failed": "Could not backup the '{part}' system part",
"backup_unable_to_organize_files": "Could not use the quick method to organize files in the archive",
- "backup_with_no_backup_script_for_app": "The app '{app:s}' has no backup script. Ignoring.",
- "backup_with_no_restore_script_for_app": "{app:s} has no restoration script, you will not be able to automatically restore the backup of this app.",
+ "backup_with_no_backup_script_for_app": "The app '{app}' has no backup script. Ignoring.",
+ "backup_with_no_restore_script_for_app": "{app} has no restoration script, you will not be able to automatically restore the backup of this app.",
"certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be ran for {domain} right now because its nginx conf lacks the corresponding code snippet... Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.",
- "certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain:s}' is not issued by Let's Encrypt. Cannot renew it automatically!",
- "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain:s}' is not about to expire! (You may use --force if you know what you're doing)",
- "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)",
- "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}",
- "certmanager_cert_install_success": "Let's Encrypt certificate now installed for the domain '{domain:s}'",
- "certmanager_cert_install_success_selfsigned": "Self-signed certificate now installed for the domain '{domain:s}'",
- "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain:s}'",
+ "certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain}' is not issued by Let's Encrypt. Cannot renew it automatically!",
+ "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain}' is not about to expire! (You may use --force if you know what you're doing)",
+ "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain}! (Use --force to bypass)",
+ "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain} (file: {file}), reason: {reason}",
+ "certmanager_cert_install_success": "Let's Encrypt certificate now installed for the domain '{domain}'",
+ "certmanager_cert_install_success_selfsigned": "Self-signed certificate now installed for the domain '{domain}'",
+ "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain}'",
"certmanager_cert_signing_failed": "Could not sign the new certificate",
- "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain:s} did not work...",
+ "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work...",
+ "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' is different from this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)",
+ "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off those checks.)",
"certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off those checks.)",
- "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain:s}' is different from this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off those checks.)",
- "certmanager_domain_http_not_working": "Domain {domain:s} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off those checks.)",
- "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain:s}' does not resolve to the same IP address as '{domain:s}'. Some features will not be available until you fix this and regenerate the certificate.",
- "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details",
- "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain:s} (file: {file:s})",
- "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file:s})",
- "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file:s})",
- "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers:s}] ",
- "confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers:s}'",
- "confirm_app_install_thirdparty": "DANGER! This app is not part of Yunohost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers:s}'",
- "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}",
+ "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details",
+ "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})",
+ "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})",
+ "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})",
+ "certmanager_warning_subdomain_dns_record": "Subdomain '{subdomain}' does not resolve to the same IP address as '{domain}'. Some features will not be available until you fix this and regenerate the certificate.",
+ "config_apply_failed": "Applying the new configuration failed: {error}",
+ "config_cant_set_value_on_section": "You can't set a single value on an entire config section.",
+ "config_forbidden_keyword": "The keyword '{keyword}' is reserved, you can't create or use a config panel with a question with this id.",
+ "config_no_panel": "No config panel found.",
+ "config_unknown_filter_key": "The filter key '{filter_key}' is incorrect.",
+ "config_validate_color": "Should be a valid RGB hexadecimal color",
+ "config_validate_date": "Should be a valid date like in the format YYYY-MM-DD",
+ "config_validate_email": "Should be a valid email",
+ "config_validate_time": "Should be a valid time like HH:MM",
+ "config_validate_url": "Should be a valid web URL",
+ "config_version_not_supported": "Config panel versions '{version}' are not supported.",
+ "confirm_app_install_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'",
+ "confirm_app_install_thirdparty": "DANGER! This app is not part of YunoHost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system... If you are willing to take that risk anyway, type '{answers}'",
+ "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ",
+ "custom_app_url_required": "You must provide a URL to upgrade your custom app {app}",
+ "danger": "Danger:",
+ "diagnosis_apps_allgood": "All installed apps respect basic packaging practices",
+ "diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.",
+ "diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.",
+ "diagnosis_apps_deprecated_practices": "This app's installed version still uses some super-old deprecated packaging practices. You should really consider upgrading it.",
+ "diagnosis_apps_issue": "An issue was found for app {app}",
+ "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and got removed, you should consider uninstalling this app as it won't receive upgrade, and may compromise the integrity and security of your system.",
+ "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.",
+ "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.",
"diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}",
"diagnosis_basesystem_hardware_model": "Server model is {model}",
"diagnosis_basesystem_host": "Server is running Debian {debian_version}",
"diagnosis_basesystem_kernel": "Server is running Linux kernel {kernel_version}",
- "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})",
- "diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})",
"diagnosis_basesystem_ynh_inconsistent_versions": "You are running inconsistent versions of the YunoHost packages... most probably because of a failed or partial upgrade.",
- "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.",
- "diagnosis_package_installed_from_sury": "Some system packages should be downgraded",
- "diagnosis_package_installed_from_sury_details": "Some packages were inadvertendly installed from a third-party repository called Sury. The Yunohost team improved the strategy that handle these packages, but it's expected that some setups that installed PHP7.3 apps while still on Stretch have some remaining inconsistencies. To fix this situation, you should try running the following command: {cmd_to_fix} ",
- "diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues' from the command-line.",
- "diagnosis_failed_for_category": "Diagnosis failed for category '{category}': {error}",
+ "diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})",
+ "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})",
"diagnosis_cache_still_valid": "(Cache still valid for {category} diagnosis. Won't re-diagnose it yet!)",
"diagnosis_cant_run_because_of_dep": "Can't run diagnosis for {category} while there are important issues related to {dep}.",
- "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))",
- "diagnosis_found_errors": "Found {errors} significant issue(s) related to {category}!",
- "diagnosis_found_errors_and_warnings": "Found {errors} significant issue(s) (and {warnings} warning(s)) related to {category}!",
- "diagnosis_found_warnings": "Found {warnings} item(s) that could be improved for {category}.",
- "diagnosis_everything_ok": "Everything looks good for {category}!",
- "diagnosis_failed": "Failed to fetch diagnosis result for category '{category}': {error}",
- "diagnosis_no_cache": "No diagnosis cache yet for category '{category}'",
- "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!",
- "diagnosis_ip_no_ipv4": "The server does not have working IPv4.",
- "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!",
- "diagnosis_ip_no_ipv6": "The server does not have working IPv6.",
- "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6. If you cannot enable IPv6 or if it seems too technical for you, you can also safely ignore this warning.",
- "diagnosis_ip_global": "Global IP: {global}
",
- "diagnosis_ip_local": "Local IP: {local}
",
- "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?",
- "diagnosis_ip_dnsresolution_working": "Domain name resolution is working!",
- "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?",
- "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf
not pointing to 127.0.0.1
.",
- "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf
.",
- "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf
should be a symlink to /etc/resolvconf/run/resolv.conf
itself pointing to 127.0.0.1
(dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf
.",
- "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})",
+ "diagnosis_description_apps": "Applications",
+ "diagnosis_description_basesystem": "Base system",
+ "diagnosis_description_dnsrecords": "DNS records",
+ "diagnosis_description_ip": "Internet connectivity",
+ "diagnosis_description_mail": "Email",
+ "diagnosis_description_ports": "Ports exposure",
+ "diagnosis_description_regenconf": "System configurations",
+ "diagnosis_description_services": "Services status check",
+ "diagnosis_description_systemresources": "System resources",
+ "diagnosis_description_web": "Web",
+ "diagnosis_diskusage_low": "Storage {mountpoint}
(on device {device}
) has only {free} ({free_percent}%) space remaining (out of {total}). Be careful.",
+ "diagnosis_diskusage_ok": "Storage {mountpoint}
(on device {device}
) still has {free} ({free_percent}%) space left (out of {total})!",
+ "diagnosis_diskusage_verylow": "Storage {mountpoint}
(on device {device}
) has only {free} ({free_percent}%) space remaining (out of {total}). You should really consider cleaning up some space!",
+ "diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues --human-readable' from the command-line.",
"diagnosis_dns_bad_conf": "Some DNS records are missing or incorrect for domain {domain} (category {category})",
- "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}
",
"diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}
",
+ "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})",
+ "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {value}
",
"diagnosis_dns_point_to_doc": "Please check the documentation at https://yunohost.org/dns_config if you need help about configuring DNS records.",
- "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by Yunohost. If that's not the case, you can try to force an update using yunohost dyndns update --force .",
+ "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.",
+ "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force .",
+ "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!",
"diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains",
- "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!",
"diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?",
"diagnosis_domain_expiration_success": "Your domains are registered and not going to expire anytime soon.",
"diagnosis_domain_expiration_warning": "Some domains will expire soon!",
- "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!",
"diagnosis_domain_expires_in": "{domain} expires in {days} days.",
- "diagnosis_services_running": "Service {service} is running!",
- "diagnosis_services_conf_broken": "Configuration is broken for service {service}!",
- "diagnosis_services_bad_status": "Service {service} is {status} :(",
- "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service} ).",
- "diagnosis_diskusage_verylow": "Storage {mountpoint}
(on device {device}
) has only {free} ({free_percent}%) space remaining (out of {total}). You should really consider cleaning up some space!",
- "diagnosis_diskusage_low": "Storage {mountpoint}
(on device {device}
) has only {free} ({free_percent}%) space remaining (out of {total}). Be careful.",
- "diagnosis_diskusage_ok": "Storage {mountpoint}
(on device {device}
) still has {free} ({free_percent}%) space left (out of {total})!",
- "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})",
+ "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!",
+ "diagnosis_everything_ok": "Everything looks OK for {category}!",
+ "diagnosis_failed": "Failed to fetch diagnosis result for category '{category}': {error}",
+ "diagnosis_failed_for_category": "Diagnosis failed for category '{category}': {error}",
+ "diagnosis_found_errors": "Found {errors} significant issue(s) related to {category}!",
+ "diagnosis_found_errors_and_warnings": "Found {errors} significant issue(s) (and {warnings} warning(s)) related to {category}!",
+ "diagnosis_found_warnings": "Found {warnings} item(s) that could be improved for {category}.",
+ "diagnosis_high_number_auth_failures": "There's been a suspiciously high number of authentication failures recently. You may want to make sure that fail2ban is running and is correctly configured, or use a custom port for SSH as explained in https://yunohost.org/security.",
+ "diagnosis_http_bad_status_code": "It looks like another machine (maybe your internet router) answered instead of your server.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.",
+ "diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.",
+ "diagnosis_http_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.",
+ "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network",
+ "diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.",
+ "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.",
+ "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok, apply the changes with yunohost tools regen-conf nginx --force .",
+ "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.",
+ "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.",
+ "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} ignored issue(s))",
+ "diagnosis_ip_broken_dnsresolution": "Domain name resolution seems to be broken for some reason... Is a firewall blocking DNS requests ?",
+ "diagnosis_ip_broken_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf
not pointing to 127.0.0.1
.",
+ "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!",
+ "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!",
+ "diagnosis_ip_dnsresolution_working": "Domain name resolution is working!",
+ "diagnosis_ip_global": "Global IP: {global}
",
+ "diagnosis_ip_local": "Local IP: {local}
",
+ "diagnosis_ip_no_ipv4": "The server does not have working IPv4.",
+ "diagnosis_ip_no_ipv6": "The server does not have working IPv6.",
+ "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6. If you cannot enable IPv6 or if it seems too technical for you, you can also safely ignore this warning.",
+ "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?",
+ "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf
.",
+ "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf
should be a symlink to /etc/resolvconf/run/resolv.conf
itself pointing to 127.0.0.1
(dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf
.",
+ "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item}
is blacklisted on {blacklist_name}",
+ "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted",
+ "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}",
+ "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for your IP or domaine to be removed on {blacklist_website}",
+ "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}",
+ "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.",
+ "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.",
+ "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!",
+ "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.",
+ "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.",
+ "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}
",
+ "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider",
+ "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set smtp.allow_ipv6 -v off . Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.",
+ "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain}
in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).",
+ "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!",
+ "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.",
+ "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider",
+ "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).",
+ "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues",
+ "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)",
+ "diagnosis_mail_queue_unavailable": "Can not consult number of pending emails in queue",
+ "diagnosis_mail_queue_unavailable_details": "Error: {error}",
+ "diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.",
+ "diagnosis_no_cache": "No diagnosis cache yet for category '{category}'",
+ "diagnosis_package_installed_from_sury": "Some system packages should be downgraded",
+ "diagnosis_package_installed_from_sury_details": "Some packages were inadvertendly installed from a third-party repository called Sury. The YunoHost team improved the strategy that handle these packages, but it's expected that some setups that installed PHP7.3 apps while still on Stretch have some remaining inconsistencies. To fix this situation, you should try running the following command: {cmd_to_fix} ",
+ "diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.",
+ "diagnosis_ports_could_not_diagnose_details": "Error: {error}",
+ "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config",
+ "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})",
+ "diagnosis_ports_ok": "Port {port} is reachable from outside.",
+ "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.",
+ "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.",
+ "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}",
"diagnosis_ram_low": "The system has {available} ({available_percent}%) RAM available (out of {total}). Be careful.",
"diagnosis_ram_ok": "The system still has {available} ({available_percent}%) RAM available out of {total}.",
+ "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})",
+ "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!",
+ "diagnosis_regenconf_manually_modified": "Configuration file {file}
appears to have been manually modified.",
+ "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force ",
+ "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.",
+ "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.",
+ "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability",
+ "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.",
+ "diagnosis_services_bad_status": "Service {service} is {status} :(",
+ "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service} ).",
+ "diagnosis_services_conf_broken": "Configuration is broken for service {service}!",
+ "diagnosis_services_running": "Service {service} is running!",
+ "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.port' is available to avoid manually editing the configuration.",
+ "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.",
+ "diagnosis_sshd_config_insecure": "The SSH configuration appears to have been manually modified, and is insecure because it contains no 'AllowGroups' or 'AllowUsers' directive to limit access to authorized users.",
"diagnosis_swap_none": "The system has no swap at all. You should consider adding at least {recommended} of swap to avoid situations where the system runs out of memory.",
"diagnosis_swap_notsomuch": "The system has only {total} swap. You should consider having at least {recommended} to avoid situations where the system runs out of memory.",
"diagnosis_swap_ok": "The system has {total} of swap!",
"diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device`.",
- "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).",
- "diagnosis_mail_outgoing_port_25_blocked": "The SMTP mail server cannot send emails to other servers because outgoing port 25 is blocked in IPv{ipversion}.",
- "diagnosis_mail_outgoing_port_25_blocked_details": "You should first try to unblock outgoing port 25 in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).",
- "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Some providers won't let you unblock outgoing port 25 because they don't care about Net Neutrality.
- Some of them provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider",
- "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!",
- "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.",
- "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
- "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}",
- "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an other machine answering instead of your server.",
- "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.",
- "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.",
- "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from outside in IPv{ipversion}.",
- "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}",
- "diagnosis_mail_fcrdns_ok": "Your reverse DNS is correctly configured!",
- "diagnosis_mail_fcrdns_dns_missing": "No reverse DNS is defined in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.",
- "diagnosis_mail_fcrdns_nok_details": "You should first try to configure the reverse DNS with {ehlo_domain}
in your internet router interface or your hosting provider interface. (Some hosting provider may require you to send them a support ticket for this).",
- "diagnosis_mail_fcrdns_nok_alternatives_4": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If you are experiencing issues because of this, consider the following solutions:
- Some ISP provide the alternative of using a mail server relay though it implies that the relay will be able to spy on your email traffic.
- A privacy-friendly alternative is to use a VPN *with a dedicated public IP* to bypass this kind of limits. See https://yunohost.org/#/vpn_advantage
- Or it's possible to switch to a different provider",
- "diagnosis_mail_fcrdns_nok_alternatives_6": "Some providers won't let you configure your reverse DNS (or their feature might be broken...). If your reverse DNS is correctly configured for IPv4, you can try disabling the use of IPv6 when sending emails by running yunohost settings set smtp.allow_ipv6 -v off . Note: this last solution means that you won't be able to send or receive emails from the few IPv6-only servers out there.",
- "diagnosis_mail_fcrdns_different_from_ehlo_domain": "The reverse DNS is not correctly configured in IPv{ipversion}. Some emails may fail to get delivered or may get flagged as spam.",
- "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Current reverse DNS: {rdns_domain}
Expected value: {ehlo_domain}
",
- "diagnosis_mail_blacklist_ok": "The IPs and domains used by this server do not appear to be blacklisted",
- "diagnosis_mail_blacklist_listed_by": "Your IP or domain {item}
is blacklisted on {blacklist_name}",
- "diagnosis_mail_blacklist_reason": "The blacklist reason is: {reason}",
- "diagnosis_mail_blacklist_website": "After identifying why you are listed and fixed it, feel free to ask for your IP or domaine to be removed on {blacklist_website}",
- "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues",
- "diagnosis_mail_queue_unavailable": "Can not consult number of pending emails in queue",
- "diagnosis_mail_queue_unavailable_details": "Error: {error}",
- "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)",
- "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!",
- "diagnosis_regenconf_manually_modified": "Configuration file {file}
appears to have been manually modified.",
- "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force ",
- "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.",
- "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.",
- "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability",
- "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.",
- "diagnosis_description_basesystem": "Base system",
- "diagnosis_description_ip": "Internet connectivity",
- "diagnosis_description_dnsrecords": "DNS records",
- "diagnosis_description_services": "Services status check",
- "diagnosis_description_systemresources": "System resources",
- "diagnosis_description_ports": "Ports exposure",
- "diagnosis_description_web": "Web",
- "diagnosis_description_mail": "Email",
- "diagnosis_description_regenconf": "System configurations",
- "diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.",
- "diagnosis_ports_could_not_diagnose_details": "Error: {error}",
- "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.",
- "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from outside in IPv{failed}.",
- "diagnosis_ports_ok": "Port {port} is reachable from outside.",
- "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})",
- "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://yunohost.org/isp_box_config",
- "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.",
- "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://yunohost.org/dns_local_network",
- "diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.",
- "diagnosis_http_could_not_diagnose_details": "Error: {error}",
- "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.",
- "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. You should also make sure that the service nginx is running
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
- "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.",
- "diagnosis_http_bad_status_code": "It looks like another machine (maybe your internet router) answered instead of your server.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
- "diagnosis_http_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network.",
- "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.",
- "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.",
- "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok, apply the changes with yunohost tools regen-conf nginx --force .",
"diagnosis_unknown_categories": "The following categories are unknown: {categories}",
- "diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.",
- "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}",
- "domain_cannot_remove_main": "You cannot remove '{domain:s}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains:s}",
+ "disk_space_not_sufficient_install": "There is not enough disk space left to install this application",
+ "disk_space_not_sufficient_update": "There is not enough disk space left to update this application",
"domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.",
"domain_cannot_add_muc_upload": "You cannot add domains starting with 'muc.'. This kind of name is reserved for the XMPP multi-users chat feature integrated in YunoHost.",
- "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain:s}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain:s}' using 'yunohost domain remove {domain:s}'.'",
+ "domain_cannot_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}",
+ "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.'",
"domain_cert_gen_failed": "Could not generate certificate",
"domain_created": "Domain created",
"domain_creation_failed": "Unable to create domain {domain}: {error}",
"domain_deleted": "Domain deleted",
"domain_deletion_failed": "Unable to delete domain {domain}: {error}",
"domain_dns_conf_is_just_a_recommendation": "This command shows you the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.",
+ "domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.",
"domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain",
"domain_dyndns_root_unknown": "Unknown DynDNS root domain",
"domain_exists": "The domain already exists",
"domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).",
- "domain_uninstall_app_first": "Those applications are still installed on your domain:\n{apps}\n\nPlease uninstall them using 'yunohost app remove the_app_id' or move them to another domain using 'yunohost app change-url the_app_id' before proceeding to domain removal",
"domain_name_unknown": "Domain '{domain}' unknown",
- "domain_unknown": "Unknown domain",
+ "domain_remove_confirm_apps_removal": "Removing this domain will remove those applications:\n{apps}\n\nAre you sure you want to do that? [{answers}]",
+ "domain_uninstall_app_first": "Those applications are still installed on your domain:\n{apps}\n\nPlease uninstall them using 'yunohost app remove the_app_id' or move them to another domain using 'yunohost app change-url the_app_id' before proceeding to domain removal",
+ "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.",
+ "domain_dns_push_not_applicable": "The automatic DNS configuration feature is not applicable to domain {domain}. You should manually configure your DNS records following the documentation at https://yunohost.org/dns_config.",
+ "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.",
+ "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.",
+ "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)",
+ "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://yunohost.org/dns.",
+ "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )",
+ "domain_dns_registrar_experimental": "So far, the interface with **{registrar}**'s API has not been properly tested and reviewed by the YunoHost community. Support is **very experimental** - be careful!",
+ "domain_dns_push_failed_to_authenticate": "Failed to authenticate on registrar's API for domain '{domain}'. Most probably the credentials are incorrect? (Error: {error})",
+ "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}",
+ "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.",
+ "domain_dns_pushing": "Pushing DNS records...",
+ "domain_dns_push_record_failed": "Failed to {action} record {type}/{name} : {error}",
+ "domain_dns_push_success": "DNS records updated!",
+ "domain_dns_push_failed": "Updating the DNS records failed miserably.",
+ "domain_dns_push_partial_failure": "DNS records partially updated: some warnings/errors were reported.",
+ "domain_config_features_disclaimer": "So far, enabling/disabling mail or XMPP features only impact the recommended and automatic DNS configuration, not system configurations!",
+ "domain_config_mail_in": "Incoming emails",
+ "domain_config_mail_out": "Outgoing emails",
+ "domain_config_xmpp": "Instant messaging (XMPP)",
+ "domain_config_auth_token": "Authentication token",
+ "domain_config_auth_key": "Authentication key",
+ "domain_config_auth_secret": "Authentication secret",
+ "domain_config_api_protocol": "API protocol",
+ "domain_config_auth_entrypoint": "API entry point",
+ "domain_config_auth_application_key": "Application key",
+ "domain_config_auth_application_secret": "Application secret key",
+ "domain_config_auth_consumer_key": "Consumer key",
"domains_available": "Available domains:",
"done": "Done",
- "downloading": "Downloading…",
- "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state… You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.",
+ "downloading": "Downloading...",
+ "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.",
"dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)",
- "dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.",
- "dyndns_could_not_check_available": "Could not check if {domain:s} is available on {provider:s}.",
- "dyndns_cron_installed": "DynDNS cron job created",
- "dyndns_cron_remove_failed": "Could not remove the DynDNS cron job because: {error}",
- "dyndns_cron_removed": "DynDNS cron job removed",
+ "dyndns_could_not_check_available": "Could not check if {domain} is available on {provider}.",
+ "dyndns_could_not_check_provide": "Could not check if {provider} can provide {domain}.",
+ "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.",
"dyndns_ip_update_failed": "Could not update IP address to DynDNS",
"dyndns_ip_updated": "Updated your IP on DynDNS",
"dyndns_key_generating": "Generating DNS key... It may take a while.",
@@ -301,163 +359,171 @@
"dyndns_no_domain_registered": "No domain registered with DynDNS",
"dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.",
"dyndns_registered": "DynDNS domain registered",
- "dyndns_registration_failed": "Could not register DynDNS domain: {error:s}",
- "dyndns_domain_not_provided": "DynDNS provider {provider:s} cannot provide domain {domain:s}.",
- "dyndns_unavailable": "The domain '{domain:s}' is unavailable.",
- "extracting": "Extracting...",
+ "dyndns_registration_failed": "Could not register DynDNS domain: {error}",
+ "dyndns_unavailable": "The domain '{domain}' is unavailable.",
"experimental_feature": "Warning: This feature is experimental and not considered stable, you should not use it unless you know what you are doing.",
- "field_invalid": "Invalid field '{:s}'",
- "file_does_not_exist": "The file {path:s} does not exist.",
+ "extracting": "Extracting...",
+ "field_invalid": "Invalid field '{}'",
+ "file_does_not_exist": "The file {path} does not exist.",
"firewall_reload_failed": "Could not reload the firewall",
"firewall_reloaded": "Firewall reloaded",
"firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.",
- "global_settings_bad_choice_for_enum": "Bad choice for setting {setting:s}, received '{choice:s}', but available choices are: {available_choices:s}",
- "global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, expected {expected_type:s}",
- "global_settings_cant_open_settings": "Could not open settings file, reason: {reason:s}",
- "global_settings_cant_serialize_settings": "Could not serialize settings data, reason: {reason:s}",
- "global_settings_cant_write_settings": "Could not save settings file, reason: {reason:s}",
- "global_settings_key_doesnt_exists": "The key '{settings_key:s}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'",
- "global_settings_reset_success": "Previous settings now backed up to {path:s}",
+ "global_settings_bad_choice_for_enum": "Bad choice for setting {setting}, received '{choice}', but available choices are: {available_choices}",
+ "global_settings_bad_type_for_setting": "Bad type for setting {setting}, received {received_type}, expected {expected_type}",
+ "global_settings_cant_open_settings": "Could not open settings file, reason: {reason}",
+ "global_settings_cant_serialize_settings": "Could not serialize settings data, reason: {reason}",
+ "global_settings_cant_write_settings": "Could not save settings file, reason: {reason}",
+ "global_settings_key_doesnt_exists": "The key '{settings_key}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'",
+ "global_settings_reset_success": "Previous settings now backed up to {path}",
+ "global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.",
"global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server",
+ "global_settings_setting_security_experimental_enabled": "Enable experimental security features (don't enable this if you don't know what you're doing!)",
"global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)",
+ "global_settings_setting_security_nginx_redirect_to_https": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)",
"global_settings_setting_security_password_admin_strength": "Admin password strength",
"global_settings_setting_security_password_user_strength": "User password strength",
- "global_settings_setting_security_ssh_compatibility": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects)",
"global_settings_setting_security_postfix_compatibility": "Compatibility vs. security tradeoff for the Postfix server. Affects the ciphers (and other security-related aspects)",
- "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discard it and save it in /etc/yunohost/settings-unknown.json",
+ "global_settings_setting_security_ssh_compatibility": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects)",
+ "global_settings_setting_security_ssh_port": "SSH port",
+ "global_settings_setting_security_webadmin_allowlist": "IP adresses allowed to access the webadmin. Comma-separated.",
+ "global_settings_setting_security_webadmin_allowlist_enabled": "Allow only some IPs to access the webadmin.",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration",
"global_settings_setting_smtp_allow_ipv6": "Allow the use of IPv6 to receive and send mail",
"global_settings_setting_smtp_relay_host": "SMTP relay host to use in order to send mail instead of this yunohost instance. Useful if you are in one of this situation: your 25 port is blocked by your ISP or VPS provider, you have a residential IP listed on DUHL, you are not able to configure reverse DNS or this server is not directly exposed on the internet and you want use an other one to send mails.",
+ "global_settings_setting_smtp_relay_password": "SMTP relay host password",
"global_settings_setting_smtp_relay_port": "SMTP relay port",
"global_settings_setting_smtp_relay_user": "SMTP relay user account",
- "global_settings_setting_smtp_relay_password": "SMTP relay host password",
- "global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.",
- "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it is not a type supported by the system.",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "Enable SSOwat panel overlay",
+ "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key}', discard it and save it in /etc/yunohost/settings-unknown.json",
+ "global_settings_unknown_type": "Unexpected situation, the setting {setting} appears to have the type {unknown_type} but it is not a type supported by the system.",
"good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).",
"good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).",
"group_already_exist": "Group {group} already exists",
"group_already_exist_on_system": "Group {group} already exists in the system groups",
"group_already_exist_on_system_but_removing_it": "Group {group} already exists in the system groups, but YunoHost will remove it...",
+ "group_cannot_be_deleted": "The group {group} cannot be deleted manually.",
+ "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost",
+ "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.",
+ "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors",
"group_created": "Group '{group}' created",
"group_creation_failed": "Could not create the group '{group}': {error}",
- "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost",
- "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors",
- "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.",
- "group_cannot_be_deleted": "The group {group} cannot be deleted manually.",
"group_deleted": "Group '{group}' deleted",
"group_deletion_failed": "Could not delete the group '{group}': {error}",
- "group_unknown": "The group '{group:s}' is unknown",
- "group_updated": "Group '{group}' updated",
+ "group_unknown": "The group '{group}' is unknown",
"group_update_failed": "Could not update the group '{group}': {error}",
+ "group_updated": "Group '{group}' updated",
"group_user_already_in_group": "User {user} is already in group {group}",
"group_user_not_in_group": "User {user} is not in group {group}",
- "hook_exec_failed": "Could not run script: {path:s}",
- "hook_exec_not_terminated": "Script did not finish properly: {path:s}",
- "hook_json_return_error": "Could not read return from hook {path:s}. Error: {msg:s}. Raw content: {raw_content}",
+ "hook_exec_failed": "Could not run script: {path}",
+ "hook_exec_not_terminated": "Script did not finish properly: {path}",
+ "hook_json_return_error": "Could not read return from hook {path}. Error: {msg}. Raw content: {raw_content}",
"hook_list_by_invalid": "This property can not be used to list hooks",
- "hook_name_unknown": "Unknown hook name '{name:s}'",
+ "hook_name_unknown": "Unknown hook name '{name}'",
"installation_complete": "Installation completed",
- "installation_failed": "Something went wrong with the installation",
- "invalid_regex": "Invalid regex:'{regex:s}'",
+ "invalid_number": "Must be a number",
+ "invalid_number_min": "Must be greater than {min}",
+ "invalid_number_max": "Must be lesser than {max}",
+ "invalid_password": "Invalid password",
+ "invalid_regex": "Invalid regex:'{regex}'",
"ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it",
"iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it",
- "log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'",
- "log_link_to_log": "Full log of this operation: '{desc}'",
- "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}{name}'",
- "log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help",
- "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help",
- "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs",
- "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly",
+ "ldap_server_down": "Unable to reach LDAP server",
+ "ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it...",
+ "ldap_attribute_already_exists": "LDAP attribute '{attribute}' already exists with value '{value}'",
+ "log_app_action_run": "Run action of the '{}' app",
"log_app_change_url": "Change the URL of the '{}' app",
+ "log_app_config_set": "Apply config to the '{}' app",
"log_app_install": "Install the '{}' app",
+ "log_app_makedefault": "Make '{}' the default app",
"log_app_remove": "Remove the '{}' app",
"log_app_upgrade": "Upgrade the '{}' app",
- "log_app_makedefault": "Make '{}' the default app",
- "log_app_action_run": "Run action of the '{}' app",
- "log_app_config_show_panel": "Show the config panel of the '{}' app",
- "log_app_config_apply": "Apply config to the '{}' app",
"log_available_on_yunopaste": "This log is now available via {url}",
- "log_backup_restore_system": "Restore system from a backup archive",
+ "log_backup_create": "Create a backup archive",
"log_backup_restore_app": "Restore '{}' from a backup archive",
- "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive",
- "log_remove_on_failed_install": "Remove '{}' after a failed installation",
+ "log_backup_restore_system": "Restore system from a backup archive",
+ "log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'",
+ "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs",
"log_domain_add": "Add '{}' domain into system configuration",
+ "log_domain_config_set": "Update configuration for domain '{}'",
+ "log_domain_main_domain": "Make '{}' the main domain",
"log_domain_remove": "Remove '{}' domain from system configuration",
+ "log_domain_dns_push": "Push DNS records for domain '{}'",
"log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'",
"log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'",
+ "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help",
+ "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}'",
"log_letsencrypt_cert_install": "Install a Let's Encrypt certificate on '{}' domain",
+ "log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate",
+ "log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help",
+ "log_link_to_log": "Full log of this operation: '{desc}'",
+ "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly",
"log_permission_create": "Create permission '{}'",
"log_permission_delete": "Delete permission '{}'",
- "log_permission_url": "Update url related to permission '{}'",
- "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain",
- "log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate",
+ "log_permission_url": "Update URL related to permission '{}'",
"log_regen_conf": "Regenerate system configurations '{}'",
+ "log_remove_on_failed_install": "Remove '{}' after a failed installation",
+ "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive",
+ "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain",
+ "log_tools_migrations_migrate_forward": "Run migrations",
+ "log_tools_postinstall": "Postinstall your YunoHost server",
+ "log_tools_reboot": "Reboot your server",
+ "log_tools_shutdown": "Shutdown your server",
+ "log_tools_upgrade": "Upgrade system packages",
"log_user_create": "Add '{}' user",
"log_user_delete": "Delete '{}' user",
"log_user_group_create": "Create '{}' group",
"log_user_group_delete": "Delete '{}' group",
"log_user_group_update": "Update '{}' group",
- "log_user_update": "Update info for user '{}'",
- "log_user_permission_update": "Update accesses for permission '{}'",
+ "log_user_import": "Import users",
"log_user_permission_reset": "Reset permission '{}'",
- "log_domain_main_domain": "Make '{}' the main domain",
- "log_tools_migrations_migrate_forward": "Run migrations",
- "log_tools_postinstall": "Postinstall your YunoHost server",
- "log_tools_upgrade": "Upgrade system packages",
- "log_tools_shutdown": "Shutdown your server",
- "log_tools_reboot": "Reboot your server",
- "ldap_init_failed_to_create_admin": "LDAP initialization could not create admin user",
- "ldap_initialized": "LDAP initialized",
- "mail_alias_remove_failed": "Could not remove e-mail alias '{mail:s}'",
- "mail_domain_unknown": "Invalid e-mail address for domain '{domain:s}'. Please, use a domain administrated by this server.",
- "mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail:s}'",
- "mailbox_disabled": "E-mail turned off for user {user:s}",
- "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space",
+ "log_user_permission_update": "Update accesses for permission '{}'",
+ "log_user_update": "Update info for user '{}'",
+ "mail_alias_remove_failed": "Could not remove e-mail alias '{mail}'",
+ "mail_domain_unknown": "Invalid e-mail address for domain '{domain}'. Please, use a domain administrated by this server.",
+ "mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail}'",
"mail_unavailable": "This e-mail address is reserved and shall be automatically allocated to the very first user",
+ "mailbox_disabled": "E-mail turned off for user {user}",
+ "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space",
"main_domain_change_failed": "Unable to change the main domain",
"main_domain_changed": "The main domain has been changed",
"migrating_legacy_permission_settings": "Migrating legacy permission settings...",
+ "migration_0015_cleaning_up": "Cleaning up cache and packages not useful anymore...",
+ "migration_0015_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.",
+ "migration_0015_main_upgrade": "Starting main upgrade...",
+ "migration_0015_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}",
+ "migration_0015_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.",
+ "migration_0015_not_stretch": "The current Debian distribution is not Stretch!",
+ "migration_0015_patching_sources_list": "Patching the sources.lists...",
+ "migration_0015_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}",
+ "migration_0015_specific_upgrade": "Starting upgrade of system packages that needs to be upgrade independently...",
+ "migration_0015_start": "Starting migration to Buster",
+ "migration_0015_still_on_stretch_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Stretch",
+ "migration_0015_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Buster.",
+ "migration_0015_weak_certs": "The following certificates were found to still use weak signature algorithms and have to be upgraded to be compatible with the next version of nginx: {certs}",
+ "migration_0015_yunohost_upgrade": "Starting YunoHost core upgrade...",
+ "migration_0017_not_enough_space": "Make sufficient space available in {path} to run the migration.",
+ "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 is installed, but not postgresql 11‽ Something weird might have happened on your system :(...",
+ "migration_0017_postgresql_96_not_installed": "PostgreSQL was not installed on your system. Nothing to do.",
+ "migration_0018_failed_to_migrate_iptables_rules": "Failed to migrate legacy iptables rules to nftables: {error}",
+ "migration_0018_failed_to_reset_legacy_rules": "Failed to reset legacy iptables rules: {error}",
+ "migration_0019_add_new_attributes_in_ldap": "Add new attributes for permissions in LDAP database",
+ "migration_0019_slapd_config_will_be_overwritten": "It looks like you manually edited the slapd configuration. For this critical migration, YunoHost needs to force the update of the slapd configuration. The original files will be backuped in {conf_backup_folder}.",
"migration_description_0015_migrate_to_buster": "Upgrade the system to Debian Buster and YunoHost 4.x",
"migration_description_0016_php70_to_php73_pools": "Migrate php7.0-fpm 'pool' conf files to php7.3",
"migration_description_0017_postgresql_9p6_to_11": "Migrate databases from PostgreSQL 9.6 to 11",
"migration_description_0018_xtable_to_nftable": "Migrate old network traffic rules to the new nftable system",
"migration_description_0019_extend_permissions_features": "Extend/rework the app permission management system",
- "migration_0011_create_group": "Creating a group for each user...",
- "migration_0011_LDAP_update_failed": "Unable to update LDAP. Error: {error:s}",
- "migration_0011_migrate_permission": "Migrating permissions from apps settings to LDAP...",
- "migration_0011_update_LDAP_database": "Updating LDAP database...",
- "migration_0011_update_LDAP_schema": "Updating LDAP schema...",
- "migration_0011_failed_to_remove_stale_object": "Unable to remove stale object {dn}: {error}",
- "migration_0015_start" : "Starting migration to Buster",
- "migration_0015_patching_sources_list": "Patching the sources.lists...",
- "migration_0015_main_upgrade": "Starting main upgrade...",
- "migration_0015_still_on_stretch_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Stretch",
- "migration_0015_yunohost_upgrade" : "Starting YunoHost core upgrade...",
- "migration_0015_not_stretch" : "The current Debian distribution is not Stretch!",
- "migration_0015_not_enough_free_space" : "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.",
- "migration_0015_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Buster.",
- "migration_0015_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.",
- "migration_0015_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}",
- "migration_0015_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}",
- "migration_0015_specific_upgrade": "Starting upgrade of system packages that needs to be upgrade independently...",
- "migration_0015_cleaning_up": "Cleaning up cache and packages not useful anymore...",
- "migration_0015_weak_certs": "The following certificates were found to still use weak signature algorithms and have to be upgraded to be compatible with the next version of nginx: {certs}",
- "migration_0017_postgresql_96_not_installed": "PostgreSQL was not installed on your system. Nothing to do.",
- "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 is installed, but not postgresql 11‽ Something weird might have happened on your system :(...",
- "migration_0017_not_enough_space": "Make sufficient space available in {path} to run the migration.",
- "migration_0018_failed_to_migrate_iptables_rules": "Failed to migrate legacy iptables rules to nftables: {error}",
- "migration_0018_failed_to_reset_legacy_rules": "Failed to reset legacy iptables rules: {error}",
- "migration_0019_add_new_attributes_in_ldap": "Add new attributes for permissions in LDAP database",
- "migration_0019_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.",
- "migration_0019_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error:s}",
- "migration_0019_migration_failed_trying_to_rollback": "Could not migrate... trying to roll back the system.",
- "migration_0019_rollback_success": "System rolled back.",
- "migration_0019_slapd_config_will_be_overwritten": "It looks like you manually edited the slapd configuration. For this critical migration, YunoHost needs to force the update of the slapd configuration. The original files will be backuped in {conf_backup_folder}.",
+ "migration_description_0020_ssh_sftp_permissions": "Add SSH and SFTP permissions support",
+ "migration_ldap_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.",
+ "migration_ldap_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error}",
+ "migration_ldap_migration_failed_trying_to_rollback": "Could not migrate... trying to roll back the system.",
+ "migration_ldap_rollback_success": "System rolled back.",
+ "migration_update_LDAP_schema": "Updating LDAP schema...",
"migrations_already_ran": "Those migrations are already done: {ids}",
"migrations_cant_reach_migration_file": "Could not access migrations files at the path '%s'",
"migrations_dependencies_not_satisfied": "Run these migrations: '{dependencies_id}', before migration {id}.",
- "migrations_failed_to_load_migration": "Could not load migration {id}: {error}",
"migrations_exclusive_options": "'--auto', '--skip', and '--force-rerun' are mutually exclusive options.",
+ "migrations_failed_to_load_migration": "Could not load migration {id}: {error}",
"migrations_list_conflict_pending_done": "You cannot use both '--previous' and '--done' at the same time.",
"migrations_loading_migration": "Loading migration {id}...",
"migrations_migration_has_failed": "Migration {id} did not complete, aborting. Error: {exception}",
@@ -470,10 +536,10 @@
"migrations_running_forward": "Running migration {id}...",
"migrations_skip_migration": "Skipping migration {id}...",
"migrations_success_forward": "Migration {id} completed",
- "migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations run`.",
- "not_enough_disk_space": "Not enough free space on '{path:s}'",
- "invalid_number": "Must be a number",
+ "migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations run`.",
+ "not_enough_disk_space": "Not enough free space on '{path}'",
"operation_interrupted": "The operation was manually interrupted?",
+ "other_available_options": "... and {n} other available options not shown",
"packages_upgrade_failed": "Could not upgrade all the packages",
"password_listed": "This password is among the most used passwords in the world. Please choose something more unique.",
"password_too_simple_1": "The password needs to be at least 8 characters long",
@@ -482,34 +548,36 @@
"password_too_simple_4": "The password needs to be at least 12 characters long and contain a digit, upper, lower and special characters",
"pattern_backup_archive_name": "Must be a valid filename with max 30 characters, alphanumeric and -_. characters only",
"pattern_domain": "Must be a valid domain name (e.g. my-domain.org)",
- "pattern_email_forward": "Must be a valid e-mail address, '+' symbol accepted (e.g. someone+tag@example.com)",
"pattern_email": "Must be a valid e-mail address, without '+' symbol (e.g. someone@example.com)",
+ "pattern_email_forward": "Must be a valid e-mail address, '+' symbol accepted (e.g. someone+tag@example.com)",
"pattern_firstname": "Must be a valid first name",
"pattern_lastname": "Must be a valid last name",
"pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to not have a quota",
"pattern_password": "Must be at least 3 characters long",
- "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)",
- "pattern_positive_number": "Must be a positive number",
- "pattern_username": "Must be lower-case alphanumeric and underscore characters only",
"pattern_password_app": "Sorry, passwords can not contain the following characters: {forbidden_chars}",
+ "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)",
+ "pattern_username": "Must be lower-case alphanumeric and underscore characters only",
"permission_already_allowed": "Group '{group}' already has permission '{permission}' enabled",
"permission_already_disallowed": "Group '{group}' already has permission '{permission}' disabled",
"permission_already_exist": "Permission '{permission}' already exists",
"permission_already_up_to_date": "The permission was not updated because the addition/removal requests already match the current state.",
"permission_cannot_remove_main": "Removing a main permission is not allowed",
- "permission_created": "Permission '{permission:s}' created",
+ "permission_cant_add_to_all_users": "The permission {permission} can not be added to all users.",
+ "permission_created": "Permission '{permission}' created",
"permission_creation_failed": "Could not create permission '{permission}': {error}",
"permission_currently_allowed_for_all_users": "This permission is currently granted to all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups it is currently granted to.",
- "permission_deleted": "Permission '{permission:s}' deleted",
+ "permission_deleted": "Permission '{permission}' deleted",
"permission_deletion_failed": "Could not delete permission '{permission}': {error}",
- "permission_not_found": "Permission '{permission:s}' not found",
- "permission_update_failed": "Could not update permission '{permission}': {error}",
- "permission_updated": "Permission '{permission:s}' updated",
+ "permission_not_found": "Permission '{permission}' not found",
"permission_protected": "Permission {permission} is protected. You cannot add or remove the visitors group to/from this permission.",
"permission_require_account": "Permission {permission} only makes sense for users having an account, and therefore cannot be enabled for visitors.",
- "port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections",
- "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections",
+ "permission_update_failed": "Could not update permission '{permission}': {error}",
+ "permission_updated": "Permission '{permission}' updated",
+ "port_already_closed": "Port {port} is already closed for {ip_version} connections",
+ "port_already_opened": "Port {port} is already opened for {ip_version} connections",
"postinstall_low_rootfsspace": "The root filesystem has a total space less than 10 GB, which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16GB for the root filesystem. If you want to install YunoHost despite this warning, re-run the postinstall with --force-diskspace",
+ "regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'...",
+ "regenconf_failed": "Could not regenerate the configuration for category(s): {categories}",
"regenconf_file_backed_up": "Configuration file '{conf}' backed up to '{backup}'",
"regenconf_file_copy_failed": "Could not copy the new configuration file '{new}' to '{conf}'",
"regenconf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by regen-conf (category {category}) but was kept back.",
@@ -518,44 +586,41 @@
"regenconf_file_remove_failed": "Could not remove the configuration file '{conf}'",
"regenconf_file_removed": "Configuration file '{conf}' removed",
"regenconf_file_updated": "Configuration file '{conf}' updated",
+ "regenconf_need_to_explicitly_specify_ssh": "The ssh configuration has been manually modified, but you need to explicitly specify category 'ssh' with --force to actually apply the changes.",
"regenconf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost (category {category}).",
+ "regenconf_pending_applying": "Applying pending configuration for category '{category}'...",
"regenconf_up_to_date": "The configuration is already up-to-date for category '{category}'",
"regenconf_updated": "Configuration updated for '{category}'",
"regenconf_would_be_updated": "The configuration would have been updated for category '{category}'",
- "regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'…",
- "regenconf_failed": "Could not regenerate the configuration for category(s): {categories}",
- "regenconf_pending_applying": "Applying pending configuration for category '{category}'...",
- "regenconf_need_to_explicitly_specify_ssh": "The ssh configuration has been manually modified, but you need to explicitly specify category 'ssh' with --force to actually apply the changes.",
"regex_incompatible_with_tile": "/!\\ Packagers! Permission '{permission}' has show_tile set to 'true' and you therefore cannot define a regex URL as the main URL",
"regex_with_only_domain": "You can't use a regex for domain, only for path",
- "restore_already_installed_app": "An app with the ID '{app:s}' is already installed",
+ "restore_already_installed_app": "An app with the ID '{app}' is already installed",
"restore_already_installed_apps": "The following apps can't be restored because they are already installed: {apps}",
- "restore_app_failed": "Could not restore {app:s}",
+ "restore_backup_too_old": "This backup archive can not be restored because it comes from a too-old YunoHost version.",
"restore_cleaning_failed": "Could not clean up the temporary restoration directory",
"restore_complete": "Restoration completed",
- "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]",
- "restore_extracting": "Extracting needed files from the archive…",
+ "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers}]",
+ "restore_extracting": "Extracting needed files from the archive...",
"restore_failed": "Could not restore system",
- "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either",
- "restore_may_be_not_enough_disk_space": "Your system does not seem to have enough space (free: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)",
- "restore_not_enough_disk_space": "Not enough space (space: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)",
+ "restore_hook_unavailable": "Restoration script for '{part}' not available on your system and not in the archive either",
+ "restore_may_be_not_enough_disk_space": "Your system does not seem to have enough space (free: {free_space} B, needed space: {needed_space} B, security margin: {margin} B)",
+ "restore_not_enough_disk_space": "Not enough space (space: {free_space} B, needed space: {needed_space} B, security margin: {margin} B)",
"restore_nothings_done": "Nothing was restored",
"restore_removing_tmp_dir_failed": "Could not remove an old temporary directory",
- "restore_running_app_script": "Restoring the app '{app:s}'…",
- "restore_running_hooks": "Running restoration hooks…",
- "restore_system_part_failed": "Could not restore the '{part:s}' system part",
+ "restore_running_app_script": "Restoring the app '{app}'...",
+ "restore_running_hooks": "Running restoration hooks...",
+ "restore_system_part_failed": "Could not restore the '{part}' system part",
"root_password_desynchronized": "The admin password was changed, but YunoHost could not propagate this to the root password!",
"root_password_replaced_by_admin_password": "Your root password have been replaced by your admin password.",
- "server_shutdown": "The server will shut down",
- "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]",
"server_reboot": "The server will reboot",
- "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]",
- "service_add_failed": "Could not add the service '{service:s}'",
- "service_added": "The service '{service:s}' was added",
- "service_already_started": "The service '{service:s}' is running already",
- "service_already_stopped": "The service '{service:s}' has already been stopped",
- "service_cmd_exec_failed": "Could not execute the command '{command:s}'",
- "service_description_avahi-daemon": "Allows you to reach your server using 'yunohost.local' in your local network",
+ "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers}]",
+ "server_shutdown": "The server will shut down",
+ "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers}]",
+ "service_add_failed": "Could not add the service '{service}'",
+ "service_added": "The service '{service}' was added",
+ "service_already_started": "The service '{service}' is running already",
+ "service_already_stopped": "The service '{service}' has already been stopped",
+ "service_cmd_exec_failed": "Could not execute the command '{command}'",
"service_description_dnsmasq": "Handles domain name resolution (DNS)",
"service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)",
"service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet",
@@ -570,45 +635,47 @@
"service_description_ssh": "Allows you to connect remotely to your server via a terminal (SSH protocol)",
"service_description_yunohost-api": "Manages interactions between the YunoHost web interface and the system",
"service_description_yunohost-firewall": "Manages open and close connection ports to services",
- "service_disable_failed": "Could not make the service '{service:s}' not start at boot.\n\nRecent service logs:{logs:s}",
- "service_disabled": "The service '{service:s}' will not be started anymore when system boots.",
- "service_enable_failed": "Could not make the service '{service:s}' automatically start at boot.\n\nRecent service logs:{logs:s}",
- "service_enabled": "The service '{service:s}' will now be automatically started during system boots.",
+ "service_description_yunomdns": "Allows you to reach your server using 'yunohost.local' in your local network",
+ "service_disable_failed": "Could not make the service '{service}' not start at boot.\n\nRecent service logs:{logs}",
+ "service_disabled": "The service '{service}' will not be started anymore when system boots.",
+ "service_enable_failed": "Could not make the service '{service}' automatically start at boot.\n\nRecent service logs:{logs}",
+ "service_enabled": "The service '{service}' will now be automatically started during system boots.",
+ "service_not_reloading_because_conf_broken": "Not reloading/restarting service '{name}' because its configuration is broken: {errors}",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' is deprecated! Please use 'yunohost tools regen-conf' instead.",
- "service_remove_failed": "Could not remove the service '{service:s}'",
- "service_removed": "Service '{service:s}' removed",
- "service_reload_failed": "Could not reload the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_reloaded": "Service '{service:s}' reloaded",
- "service_restart_failed": "Could not restart the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_restarted": "Service '{service:s}' restarted",
- "service_reload_or_restart_failed": "Could not reload or restart the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_reloaded_or_restarted": "The service '{service:s}' was reloaded or restarted",
- "service_start_failed": "Could not start the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_started": "Service '{service:s}' started",
- "service_stop_failed": "Unable to stop the service '{service:s}'\n\nRecent service logs:{logs:s}",
- "service_stopped": "Service '{service:s}' stopped",
- "service_unknown": "Unknown service '{service:s}'",
+ "service_reload_failed": "Could not reload the service '{service}'\n\nRecent service logs:{logs}",
+ "service_reload_or_restart_failed": "Could not reload or restart the service '{service}'\n\nRecent service logs:{logs}",
+ "service_reloaded": "Service '{service}' reloaded",
+ "service_reloaded_or_restarted": "The service '{service}' was reloaded or restarted",
+ "service_remove_failed": "Could not remove the service '{service}'",
+ "service_removed": "Service '{service}' removed",
+ "service_restart_failed": "Could not restart the service '{service}'\n\nRecent service logs:{logs}",
+ "service_restarted": "Service '{service}' restarted",
+ "service_start_failed": "Could not start the service '{service}'\n\nRecent service logs:{logs}",
+ "service_started": "Service '{service}' started",
+ "service_stop_failed": "Unable to stop the service '{service}'\n\nRecent service logs:{logs}",
+ "service_stopped": "Service '{service}' stopped",
+ "service_unknown": "Unknown service '{service}'",
+ "show_tile_cant_be_enabled_for_regex": "You cannot enable 'show_tile' right now, because the URL for the permission '{permission}' is a regex",
"show_tile_cant_be_enabled_for_url_not_defined": "You cannot enable 'show_tile' right now, because you must first define an URL for the permission '{permission}'",
- "show_tile_cant_be_enabled_for_regex": "You cannot enable 'show_tile' right no, because the URL for the permission '{permission}' is a regex",
"ssowat_conf_generated": "SSOwat configuration regenerated",
"ssowat_conf_updated": "SSOwat configuration updated",
"system_upgraded": "System upgraded",
"system_username_exists": "Username already exists in the list of system users",
"this_action_broke_dpkg": "This action broke dpkg/APT (the system package managers)... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.",
- "tools_upgrade_at_least_one": "Please specify '--apps', or '--system'",
+ "tools_upgrade_at_least_one": "Please specify 'apps', or 'system'",
"tools_upgrade_cant_both": "Cannot upgrade both system and apps at the same time",
- "tools_upgrade_cant_hold_critical_packages": "Could not hold critical packages…",
- "tools_upgrade_cant_unhold_critical_packages": "Could not unhold critical packages…",
- "tools_upgrade_regular_packages": "Now upgrading 'regular' (non-yunohost-related) packages…",
+ "tools_upgrade_cant_hold_critical_packages": "Could not hold critical packages...",
+ "tools_upgrade_cant_unhold_critical_packages": "Could not unhold critical packages...",
+ "tools_upgrade_regular_packages": "Now upgrading 'regular' (non-yunohost-related) packages...",
"tools_upgrade_regular_packages_failed": "Could not upgrade packages: {packages_list}",
- "tools_upgrade_special_packages": "Now upgrading 'special' (yunohost-related) packages…",
- "tools_upgrade_special_packages_explanation": "The special upgrade will continue in the background. Please don't start any other actions on your server for the next ~10 minutes (depending on hardware speed). After this, you may have to re-log in to the webadmin. The upgrade log will be available in Tools → Log (in the webadmin) or using 'yunohost log list' (from the command-line).",
+ "tools_upgrade_special_packages": "Now upgrading 'special' (yunohost-related) packages...",
"tools_upgrade_special_packages_completed": "YunoHost package upgrade completed.\nPress [Enter] to get the command line back",
- "unbackup_app": "{app:s} will not be saved",
+ "tools_upgrade_special_packages_explanation": "The special upgrade will continue in the background. Please don't start any other actions on your server for the next ~10 minutes (depending on hardware speed). After this, you may have to re-log in to the webadmin. The upgrade log will be available in Tools → Log (in the webadmin) or using 'yunohost log list' (from the command-line).",
+ "unbackup_app": "{app} will not be saved",
"unexpected_error": "Something unexpected went wrong: {error}",
"unknown_main_domain_path": "Unknown domain or path for '{app}'. You need to specify a domain and a path to be able to specify a URL for permission.",
"unlimit": "No quota",
- "unrestore_app": "{app:s} will not be restored",
+ "unrestore_app": "{app} will not be restored",
"update_apt_cache_failed": "Unable to update the cache of APT (Debian's package manager). Here is a dump of the sources.list lines, which might help identify problematic lines: \n{sourceslist}",
"update_apt_cache_warning": "Something went wrong while updating the cache of APT (Debian's package manager). Here is a dump of the sources.list lines, which might help identify problematic lines: \n{sourceslist}",
"updating_apt_cache": "Fetching available upgrades for system packages...",
@@ -623,13 +690,20 @@
"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_unknown": "Unknown user: {user:s}",
+ "user_home_creation_failed": "Could not create home folder '{home}' for user",
+ "user_import_bad_file": "Your CSV file is not correctly formatted it will be ignored to avoid potential data loss",
+ "user_import_bad_line": "Incorrect line {line}: {details}",
+ "user_import_failed": "The users import operation completely failed",
+ "user_import_missing_columns": "The following columns are missing: {columns}",
+ "user_import_nothing_to_do": "No user needs to be imported",
+ "user_import_partial_failed": "The users import operation partially failed",
+ "user_import_success": "Users successfully imported",
+ "user_unknown": "Unknown user: {user}",
"user_update_failed": "Could not update user {user}: {error}",
"user_updated": "User info changed",
"yunohost_already_installed": "YunoHost is already installed",
"yunohost_configured": "YunoHost is now configured",
"yunohost_installing": "Installing YunoHost...",
"yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'",
- "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know Yunohost' parts in the admin documentation: https://yunohost.org/admindoc."
+ "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create ' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc."
}
diff --git a/locales/eo.json b/locales/eo.json
index 1a27831f2..8973e6344 100644
--- a/locales/eo.json
+++ b/locales/eo.json
@@ -1,25 +1,20 @@
{
- "admin_password_change_failed": "Ne eblas ŝanĝi pasvorton",
+ "admin_password_change_failed": "Ne povis ŝanĝi pasvorton",
"admin_password_changed": "La pasvorto de administrado estis ŝanĝita",
- "app_already_installed": "{app:s} estas jam instalita",
- "app_already_up_to_date": "{app:s} estas jam ĝisdata",
- "app_argument_required": "Parametro {name:s} estas bezonata",
- "app_change_url_identical_domains": "Malnovaj kaj novaj domajno/URL estas la sama ('{domain:s}{path:s}'), nenio fareblas.",
- "app_change_url_success": "{app:s} URL nun estas {domain:s} {path:s}",
+ "app_already_installed": "{app} estas jam instalita",
+ "app_already_up_to_date": "{app} estas jam ĝisdata",
+ "app_argument_required": "Parametro {name} estas bezonata",
+ "app_change_url_identical_domains": "Malnovaj kaj novaj domajno/URL estas la sama ('{domain}{path}'), nenio fareblas.",
+ "app_change_url_success": "{app} URL nun estas {domain} {path}",
"app_extraction_failed": "Ne povis ĉerpi la instalajn dosierojn",
"app_id_invalid": "Nevalida apo ID",
"app_install_files_invalid": "Ĉi tiuj dosieroj ne povas esti instalitaj",
"user_updated": "Uzantinformoj ŝanĝis",
- "users_available": "Uzantoj disponeblaj :",
"yunohost_already_installed": "YunoHost estas jam instalita",
- "yunohost_ca_creation_failed": "Ne povis krei atestan aŭtoritaton",
- "yunohost_ca_creation_success": "Loka atestila aŭtoritato kreiĝis.",
"yunohost_installing": "Instalante YunoHost…",
"service_description_metronome": "Mastrumas XMPP tujmesaĝilon kontojn",
"service_description_mysql": "Butikigas datumojn de programoj (SQL datumbazo)",
"service_description_nginx": "Servas aŭ permesas atingi ĉiujn retejojn gastigita sur via servilo",
- "service_description_nslcd": "Mastrumas Yunohost uzantojn konektojn per komanda linio",
- "service_description_php7.0-fpm": "Ekzekutas programojn skribitajn en PHP kun NGINX",
"service_description_postfix": "Uzita por sendi kaj ricevi retpoŝtojn",
"service_description_redis-server": "Specialita datumbazo uzita por rapida datumo atingo, atendovicoj kaj komunikadoj inter programoj",
"service_description_rspamd": "Filtras trudmesaĝojn, kaj aliaj funkcioj rilate al retpoŝto",
@@ -27,127 +22,107 @@
"service_description_ssh": "Permesas al vi konekti al via servilo kun fora terminalo (SSH protokolo)",
"service_description_yunohost-api": "Mastrumas interagojn inter la YunoHost retinterfaco kaj la sistemo",
"service_description_yunohost-firewall": "Administras malfermajn kaj fermajn konektajn havenojn al servoj",
- "service_disable_failed": "Ne povis fari la servon '{service:s}' ne komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs:s}",
- "service_disabled": "La servo '{service:s}' ne plu komenciĝos kiam sistemo ekos.",
- "action_invalid": "Nevalida ago « {action:s} »",
+ "service_disable_failed": "Ne povis fari la servon '{service}' ne komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs}",
+ "service_disabled": "La servo '{service}' ne plu komenciĝos kiam sistemo ekos.",
+ "action_invalid": "Nevalida ago « {action} »",
"admin_password": "Pasvorto de la estro",
"admin_password_too_long": "Bonvolu elekti pasvorton pli mallonga ol 127 signoj",
"already_up_to_date": "Nenio por fari. Ĉio estas jam ĝisdatigita.",
- "app_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices:s}' por la argumento '{name:s}'",
- "app_argument_invalid": "Elektu validan valoron por la argumento '{name:s}': {error:s}",
- "app_change_url_failed_nginx_reload": "Ne eblis reŝarĝi NGINX. Jen la eligo de 'nginx -t':\n{nginx_errors:s}",
+ "app_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices}' por la argumento '{name}' anstataŭ '{value}'",
+ "app_argument_invalid": "Elektu validan valoron por la argumento '{name}': {error}",
"ask_new_admin_password": "Nova administrada pasvorto",
"app_action_broke_system": "Ĉi tiu ago ŝajne rompis ĉi tiujn gravajn servojn: {services}",
"app_unsupported_remote_type": "Malkontrolita fora speco uzita por la apliko",
- "backup_archive_system_part_not_available": "Sistemo parto '{part:s}' ne haveblas en ĉi tiu rezervo",
+ "backup_archive_system_part_not_available": "Sistemo parto '{part}' ne haveblas en ĉi tiu rezervo",
"backup_abstract_method": "Ĉi tiu rezerva metodo ankoraŭ efektiviĝis",
"apps_already_up_to_date": "Ĉiuj aplikoj estas jam ĝisdatigitaj",
- "backup_borg_not_implemented": "La kopia metodo de Borg ankoraŭ ne estas efektivigita",
- "app_location_unavailable": "Ĉi tiu URL aŭ ne haveblas, aŭ konfliktas kun la jam instalita (j) apliko (j):\n{apps:s}",
- "backup_archive_app_not_found": "Ne povis trovi la programon '{app:s}' en la rezerva ar archiveivo",
- "backup_actually_backuping": "Krei rezervan ar archiveivon el la kolektitaj dosieroj …",
- "backup_method_borg_finished": "Sekurkopio en Borg finiĝis",
- "app_change_url_no_script": "La app '{app_name:s}' ankoraŭ ne subtenas URL-modifon. Eble vi devus altgradigi ĝin.",
- "app_start_install": "Instali la programon '{app}' …",
+ "app_location_unavailable": "Ĉi tiu URL aŭ ne haveblas, aŭ konfliktas kun la jam instalita (j) apliko (j):\n{apps}",
+ "backup_archive_app_not_found": "Ne povis trovi {app} en la rezerva arkivo",
+ "backup_actually_backuping": "Krei rezervan arkivon de la kolektitaj dosieroj ...",
+ "app_change_url_no_script": "La app '{app_name}' ankoraŭ ne subtenas URL-modifon. Eble vi devus altgradigi ĝin.",
+ "app_start_install": "Instali {app}...",
"backup_created": "Sekurkopio kreita",
- "app_make_default_location_already_used": "Ne povas igi la aplikon '{app}' defaŭlta sur la domajno, '{domain}' jam uziĝas de la alia app '{other_app}'",
+ "app_make_default_location_already_used": "Ne povis fari '{app}' la defaŭltan programon sur la domajno, '{domain}' estas jam uzata de '{other_app}'",
"backup_method_copy_finished": "Rezerva kopio finis",
- "app_not_properly_removed": "{app:s} ne estis ĝuste forigita",
- "backup_archive_broken_link": "Ne povis aliri la rezervan ar archiveivon (rompita ligilo al {path:s})",
- "app_requirements_checking": "Kontrolante postulatajn pakaĵojn por {app} …",
- "app_not_installed": "Ne povis trovi la aplikon '{app:s}' en la listo de instalitaj programoj: {all_apps}",
+ "app_not_properly_removed": "{app} ne estis ĝuste forigita",
+ "backup_archive_broken_link": "Ne povis aliri la rezervan ar archiveivon (rompita ligilo al {path})",
+ "app_requirements_checking": "Kontrolante bezonatajn pakaĵojn por {app} ...",
+ "app_not_installed": "Ne povis trovi {app} en la listo de instalitaj programoj: {all_apps}",
"ask_new_path": "Nova vojo",
"backup_custom_mount_error": "Propra rezerva metodo ne povis preterpasi la paŝon 'monto'",
- "app_upgrade_app_name": "Nun ĝisdatiganta {app} …",
+ "app_upgrade_app_name": "Nun ĝisdatigu {app}...",
"app_manifest_invalid": "Io misas pri la aplika manifesto: {error}",
"backup_cleaning_failed": "Ne povis purigi la provizoran rezervan dosierujon",
- "backup_invalid_archive": "Ĉi tio ne estas rezerva ar archiveivo",
"backup_creation_failed": "Ne povis krei la rezervan ar archiveivon",
- "backup_hook_unknown": "La rezerva hoko '{hook:s}' estas nekonata",
+ "backup_hook_unknown": "La rezerva hoko '{hook}' estas nekonata",
"backup_custom_backup_error": "Propra rezerva metodo ne povis preterpasi la paŝon \"sekurkopio\"",
"ask_main_domain": "Ĉefa domajno",
"backup_method_tar_finished": "TAR-rezerva ar archiveivo kreita",
"backup_cant_mount_uncompress_archive": "Ne povis munti la nekompresitan ar archiveivon kiel protektita kontraŭ skribo",
"app_action_cannot_be_ran_because_required_services_down": "Ĉi tiuj postulataj servoj devas funkcii por funkciigi ĉi tiun agon: {services}. Provu rekomenci ilin por daŭrigi (kaj eble esploru, kial ili malsupreniras).",
- "backup_copying_to_organize_the_archive": "Kopiante {size:s} MB por organizi la ar archiveivon",
+ "backup_copying_to_organize_the_archive": "Kopiante {size} MB por organizi la ar archiveivon",
"backup_output_directory_forbidden": "Elektu malsaman elirejan dosierujon. Sekurkopioj ne povas esti kreitaj en sub-dosierujoj /bin, /boot, /dev, /ktp, /lib, /root, /run, /sbin, /sys, /usr, /var aŭ /home/yunohost.backup/archives",
"backup_no_uncompress_archive_dir": "Ne ekzistas tia nekompremita arkiva dosierujo",
"password_too_simple_1": "Pasvorto devas esti almenaŭ 8 signojn longa",
- "app_upgrade_failed": "Ne povis ĝisdatigi {app:s}: {error}",
+ "app_upgrade_failed": "Ne povis ĝisdatigi {app}: {error}",
"app_upgrade_several_apps": "La sekvaj apliko estos altgradigitaj: {apps}",
"backup_archive_open_failed": "Ne povis malfermi la rezervan ar archiveivon",
"ask_lastname": "Familia nomo",
- "app_start_backup": "Kolekti dosierojn por esti subtenata por la '{app}' …",
+ "app_start_backup": "Kolekti dosierojn por esti subtenata por {app}...",
"backup_archive_name_exists": "Rezerva arkivo kun ĉi tiu nomo jam ekzistas.",
- "backup_applying_method_tar": "Krei la rezervan TAR-ar archiveivon …",
- "backup_method_custom_finished": "Propra rezerva metodo '{method:s}' finiĝis",
+ "backup_applying_method_tar": "Krei la rezervon TAR Arkivo...",
+ "backup_method_custom_finished": "Propra rezerva metodo '{method}' finiĝis",
"app_already_installed_cant_change_url": "Ĉi tiu app estas jam instalita. La URL ne povas esti ŝanĝita nur per ĉi tiu funkcio. Kontrolu en `app changeurl` se ĝi haveblas.",
- "app_not_correctly_installed": "{app:s} ŝajnas esti malĝuste instalita",
- "app_removed": "{app:s} forigita",
- "backup_delete_error": "Ne povis forigi '{path:s}'",
+ "app_not_correctly_installed": "{app} ŝajnas esti malĝuste instalita",
+ "app_removed": "{app} forigita",
+ "backup_delete_error": "Ne povis forigi '{path}'",
"backup_nothings_done": "Nenio por ŝpari",
- "backup_applying_method_custom": "Nomante la kutiman rezervan metodon '{method:s}' …",
- "backup_app_failed": "Ne eblis rezervi la programon '{app:s}'",
+ "backup_applying_method_custom": "Voki la laŭmendan rezervan metodon '{method}'...",
+ "backup_app_failed": "Ne povis subteni {app}",
"app_upgrade_some_app_failed": "Iuj aplikoj ne povis esti altgradigitaj",
- "app_start_remove": "Forigo de la apliko '{app}' …",
+ "app_start_remove": "Forigado {app}...",
"backup_output_directory_not_empty": "Vi devas elekti malplenan eligitan dosierujon",
- "backup_archive_writing_error": "Ne povis aldoni la dosierojn '{source:s}' (nomitaj en la ar theivo '{dest:s}') por esti rezervitaj en la kunpremita arkivo '{archive:s}'",
- "ask_email": "Retpoŝta adreso",
- "app_start_restore": "Restarigi la programon '{app}' …",
- "backup_applying_method_copy": "Kopiante ĉiujn dosierojn al sekurkopio …",
- "backup_couldnt_bind": "Ne povis ligi {src:s} al {dest:s}.",
+ "backup_archive_writing_error": "Ne povis aldoni la dosierojn '{source}' (nomitaj en la ar theivo '{dest}') por esti rezervitaj en la kunpremita arkivo '{archive}'",
+ "app_start_restore": "Restarigi {app}...",
+ "backup_applying_method_copy": "Kopii ĉiujn dosierojn por sekurigi...",
+ "backup_couldnt_bind": "Ne povis ligi {src} al {dest}.",
"ask_password": "Pasvorto",
"app_requirements_unmeet": "Postuloj ne estas renkontitaj por {app}, la pakaĵo {pkgname} ({version}) devas esti {spec}",
"ask_firstname": "Antaŭnomo",
- "backup_ask_for_copying_if_needed": "Ĉu vi volas realigi la sekurkopion uzante {size:s} MB provizore? (Ĉi tiu maniero estas uzata ĉar iuj dosieroj ne povus esti pretigitaj per pli efika metodo.)",
+ "backup_ask_for_copying_if_needed": "Ĉu vi volas realigi la sekurkopion uzante {size} MB provizore? (Ĉi tiu maniero estas uzata ĉar iuj dosieroj ne povus esti pretigitaj per pli efika metodo.)",
"backup_mount_archive_for_restore": "Preparante arkivon por restarigo …",
"backup_csv_creation_failed": "Ne povis krei la CSV-dosieron bezonatan por restarigo",
- "backup_archive_name_unknown": "Nekonata loka rezerva ar archiveivo nomata '{name:s}'",
- "backup_applying_method_borg": "Sendado de ĉiuj dosieroj al sekurkopio en borg-rezerva deponejo …",
+ "backup_archive_name_unknown": "Nekonata loka rezerva ar archiveivo nomata '{name}'",
"app_sources_fetch_failed": "Ne povis akiri fontajn dosierojn, ĉu la URL estas ĝusta?",
"ask_new_domain": "Nova domajno",
"app_unknown": "Nekonata apliko",
"app_not_upgraded": "La '{failed_app}' de la programo ne sukcesis ĝisdatigi, kaj sekve la nuntempaj plibonigoj de la sekvaj programoj estis nuligitaj: {apps}",
"aborting": "Aborti.",
- "app_upgraded": "{app:s} altgradigita",
+ "app_upgraded": "{app} altgradigita",
"backup_deleted": "Rezerva forigita",
"backup_csv_addition_failed": "Ne povis aldoni dosierojn al sekurkopio en la CSV-dosiero",
"dpkg_lock_not_available": "Ĉi tiu komando ne povas funkcii nun ĉar alia programo uzas la seruron de dpkg (la administrilo de paka sistemo)",
- "migration_0003_yunohost_upgrade": "Komencante la ĝisdatigon de la pakaĵo YunoHost ... La migrado finiĝos, sed la efektiva ĝisdatigo okazos tuj poste. Post kiam la operacio finiĝos, vi eble devos ensaluti al la retpaĝo.",
"domain_dyndns_root_unknown": "Nekonata radika domajno DynDNS",
- "field_invalid": "Nevalida kampo '{:s}'",
+ "field_invalid": "Nevalida kampo '{}'",
"log_app_makedefault": "Faru '{}' la defaŭlta apliko",
- "migration_0003_still_on_jessie_after_main_upgrade": "Io okazis malbone dum la ĉefa ĝisdatigo: Ĉu la sistemo ankoraŭ estas en Jessie‽ Por esplori la aferon, bonvolu rigardi {log}:s …",
- "migration_0011_can_not_backup_before_migration": "La sekurkopio de la sistemo ne povis finiĝi antaŭ ol la migrado malsukcesis. Eraro: {error:s}",
- "migration_0011_create_group": "Krei grupon por ĉiu uzanto…",
- "backup_system_part_failed": "Ne eblis sekurkopi la sistemon de '{part:s}'",
+ "backup_system_part_failed": "Ne eblis sekurkopi la sistemon de '{part}'",
"global_settings_setting_security_postfix_compatibility": "Kongruo vs sekureca kompromiso por la Postfix-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)",
- "group_unknown": "La grupo '{group:s}' estas nekonata",
- "mailbox_disabled": "Retpoŝto malŝaltita por uzanto {user:s}",
- "migration_description_0011_setup_group_permission": "Agordu uzantajn grupojn kaj permesojn por programoj kaj servoj",
- "migration_0011_backup_before_migration": "Krei sekurkopion de LDAP-datumbazo kaj agordojn antaŭ la efektiva migrado.",
- "migration_0011_migrate_permission": "Migrado de permesoj de agordoj al aplikoj al LDAP…",
- "migration_0011_migration_failed_trying_to_rollback": "Ne povis migri ... provante redakti la sistemon.",
+ "group_unknown": "La grupo '{group}' estas nekonata",
+ "mailbox_disabled": "Retpoŝto malŝaltita por uzanto {user}",
"migrations_dependencies_not_satisfied": "Rulu ĉi tiujn migradojn: '{dependencies_id}', antaŭ migrado {id}.",
"migrations_failed_to_load_migration": "Ne povis ŝarĝi migradon {id}: {error}",
"migrations_exclusive_options": "'--auto', '--skip' kaj '--force-rerun' estas reciproke ekskluzivaj ebloj.",
"migrations_must_provide_explicit_targets": "Vi devas provizi eksplicitajn celojn kiam vi uzas '--skip' aŭ '--force-rerun'",
"permission_update_failed": "Ne povis ĝisdatigi permeson '{permission}': {error}",
- "permission_updated": "Ĝisdatigita \"{permission:s}\" rajtigita",
- "permission_update_nothing_to_do": "Neniuj permesoj ĝisdatigi",
+ "permission_updated": "Ĝisdatigita \"{permission}\" rajtigita",
"tools_upgrade_cant_hold_critical_packages": "Ne povis teni kritikajn pakojn…",
"upnp_dev_not_found": "Neniu UPnP-aparato trovita",
- "migration_description_0012_postgresql_password_to_md5_authentication": "Devigu PostgreSQL-aŭtentigon uzi MD5 por lokaj ligoj",
- "migration_0011_done": "Migrado finiĝis. Vi nun kapablas administri uzantajn grupojn.",
- "migration_0011_LDAP_update_failed": "Ne povis ĝisdatigi LDAP. Eraro: {error:s}",
"pattern_password": "Devas esti almenaŭ 3 signoj longaj",
"root_password_desynchronized": "La pasvorta administranto estis ŝanĝita, sed YunoHost ne povis propagandi ĉi tion al la radika pasvorto!",
- "service_remove_failed": "Ne povis forigi la servon '{service:s}'",
- "migration_0003_fail2ban_upgrade": "Komenci la ĝisdatigon Fail2Ban…",
- "backup_permission": "Rezerva permeso por app {app:s}",
+ "service_remove_failed": "Ne povis forigi la servon '{service}'",
+ "backup_permission": "Rezerva permeso por {app}",
"log_user_group_delete": "Forigi grupon '{}'",
"log_user_group_update": "Ĝisdatigi grupon '{}'",
- "migration_0005_postgresql_94_not_installed": "PostgreSQL ne estis instalita en via sistemo. Nenio por fari.",
"dyndns_provider_unreachable": "Ne povas atingi la provizanton DynDNS {provider}: ĉu via YunoHost ne estas ĝuste konektita al la interreto aŭ la dyneta servilo malŝaltiĝas.",
"good_practices_about_user_password": "Vi nun estas por difini novan uzantan pasvorton. La pasvorto devas esti almenaŭ 8 signojn - kvankam estas bone praktiki uzi pli longan pasvorton (t.e. pasfrazon) kaj/aŭ variaĵon de signoj (majuskloj, minuskloj, ciferoj kaj specialaj signoj).",
"group_updated": "Ĝisdatigita \"{group}\" grupo",
@@ -158,17 +133,12 @@
"group_user_already_in_group": "Uzanto {user} jam estas en grupo {group}",
"group_user_not_in_group": "Uzanto {user} ne estas en grupo {group}",
"installation_complete": "Kompleta instalado",
- "log_category_404": "La loga kategorio '{category}' ne ekzistas",
"log_permission_create": "Krei permeson '{}'",
"log_permission_delete": "Forigi permeson '{}'",
"log_user_group_create": "Krei grupon '{}'",
"log_user_permission_update": "Mise à jour des accès pour la permission '{}'",
"log_user_permission_reset": "Restarigi permeson '{}'",
- "mail_forward_remove_failed": "Ne povis forigi retpoŝton plusendante '{mail:s}'",
- "migration_0011_rollback_success": "Sistemo ruliĝis reen.",
- "migration_0011_update_LDAP_database": "Ĝisdatigante LDAP-datumbazon…",
- "migration_0011_update_LDAP_schema": "Ĝisdatigante LDAP-skemon…",
- "migration_0011_failed_to_remove_stale_object": "Ne povis forigi neuzatan objekton {dn}: {error}",
+ "mail_forward_remove_failed": "Ne povis forigi retpoŝton plusendante '{mail}'",
"migrations_already_ran": "Tiuj migradoj estas jam faritaj: {ids}",
"migrations_no_such_migration": "Estas neniu migrado nomata '{id}'",
"permission_already_allowed": "Grupo '{group}' jam havas rajtigitan permeson '{permission}'",
@@ -180,14 +150,14 @@
"migrations_running_forward": "Kuranta migrado {id}…",
"migrations_success_forward": "Migrado {id} kompletigita",
"operation_interrupted": "La operacio estis permane interrompita?",
- "permission_created": "Permesita '{permission:s}' kreita",
- "permission_deleted": "Permesita \"{permission:s}\" forigita",
+ "permission_created": "Permesita '{permission}' kreita",
+ "permission_deleted": "Permesita \"{permission}\" forigita",
"permission_deletion_failed": "Ne povis forigi permeson '{permission}': {error}",
- "permission_not_found": "Permesita \"{permission:s}\" ne trovita",
- "restore_not_enough_disk_space": "Ne sufiĉa spaco (spaco: {free_space:d} B, necesa spaco: {needed_space:d} B, sekureca marĝeno: {margin:d} B)",
+ "permission_not_found": "Permesita \"{permission}\" ne trovita",
+ "restore_not_enough_disk_space": "Ne sufiĉa spaco (spaco: {free_space} B, necesa spaco: {needed_space} B, sekureca marĝeno: {margin} B)",
"tools_upgrade_regular_packages": "Nun ĝisdatigi 'regulajn' (ne-yunohost-rilatajn) pakojn …",
"tools_upgrade_special_packages_explanation": "La speciala ĝisdatigo daŭros en la fono. Bonvolu ne komenci aliajn agojn en via servilo dum la sekvaj ~ 10 minutoj (depende de la aparata rapideco). Post tio, vi eble devos re-ensaluti al la retadreso. La ĝisdatiga registro estos havebla en Iloj → Ensaluto (en la retadreso) aŭ uzante 'yunohost logliston' (el la komandlinio).",
- "unrestore_app": "App '{app:s}' ne restarigos",
+ "unrestore_app": "App '{app}' ne restarigos",
"group_created": "Grupo '{group}' kreita",
"group_creation_failed": "Ne povis krei la grupon '{group}': {error}",
"group_deleted": "Grupo '{group}' forigita",
@@ -195,257 +165,200 @@
"migrations_not_pending_cant_skip": "Tiuj migradoj ankoraŭ ne estas pritraktataj, do ne eblas preterlasi: {ids}",
"permission_already_exist": "Permesita '{permission}' jam ekzistas",
"domain_created": "Domajno kreita",
- "migrate_tsig_wait_2": "2 minutoj …",
"log_user_create": "Aldonu uzanton '{}'",
"ip6tables_unavailable": "Vi ne povas ludi kun ip6tabloj ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin",
"mail_unavailable": "Ĉi tiu retpoŝta adreso estas rezervita kaj aŭtomate estos atribuita al la unua uzanto",
- "certmanager_domain_dns_ip_differs_from_public_ip": "La DNS 'A' rekordo por la domajno '{domain:s}' diferencas de la IP de ĉi tiu servilo. Se vi lastatempe modifis vian A-registron, bonvolu atendi ĝin propagandi (iuj DNS-disvastigaj kontroliloj estas disponeblaj interrete). (Se vi scias, kion vi faras, uzu '--no-checks' por malŝalti tiujn ĉekojn.)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "La DNS 'A' rekordo por la domajno '{domain}' diferencas de la IP de ĉi tiu servilo. Se vi lastatempe modifis vian A-registron, bonvolu atendi ĝin propagandi (iuj DNS-disvastigaj kontroliloj estas disponeblaj interrete). (Se vi scias, kion vi faras, uzu '--no-checks' por malŝalti tiujn ĉekojn.)",
"tools_upgrade_special_packages_completed": "Plenumis la ĝisdatigon de pakaĵoj de YunoHost.\nPremu [Enter] por retrovi la komandlinion",
"log_remove_on_failed_install": "Forigu '{}' post malsukcesa instalado",
"regenconf_file_manually_modified": "La agorddosiero '{conf}' estis modifita permane kaj ne estos ĝisdatigita",
"regenconf_would_be_updated": "La agordo estus aktualigita por la kategorio '{category}'",
- "certmanager_cert_install_success_selfsigned": "Mem-subskribita atestilo nun instalita por la domajno '{domain:s}'",
- "global_settings_unknown_setting_from_settings_file": "Nekonata ŝlosilo en agordoj: '{setting_key:s}', forĵetu ĝin kaj konservu ĝin en /etc/yunohost/settings-unknown.json",
+ "certmanager_cert_install_success_selfsigned": "Mem-subskribita atestilo nun instalita por la domajno '{domain}'",
+ "global_settings_unknown_setting_from_settings_file": "Nekonata ŝlosilo en agordoj: '{setting_key}', forĵetu ĝin kaj konservu ĝin en /etc/yunohost/settings-unknown.json",
"regenconf_file_backed_up": "Agordodosiero '{conf}' estis rezervita al '{backup}'",
- "migration_0007_cannot_restart": "SSH ne rekomencas post provi nuligi la migradan numeron 6.",
- "migration_description_0006_sync_admin_and_root_passwords": "Sinkronigu admin kaj radikajn pasvortojn",
"iptables_unavailable": "Vi ne povas ludi kun iptables ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin",
- "global_settings_cant_write_settings": "Ne eblis konservi agordojn, tial: {reason:s}",
- "service_added": "La servo '{service:s}' estis aldonita",
+ "global_settings_cant_write_settings": "Ne eblis konservi agordojn, tial: {reason}",
+ "service_added": "La servo '{service}' estis aldonita",
"upnp_disabled": "UPnP malŝaltis",
- "service_started": "Servo '{service:s}' komenciĝis",
- "port_already_opened": "Haveno {port:d} estas jam malfermita por {ip_version:s} rilatoj",
- "installation_failed": "Io okazis malbone kun la instalado",
- "migrate_tsig_wait_3": "1 minuto …",
- "certmanager_conflicting_nginx_file": "Ne povis prepari domajnon por ACME-defio: la agordo de NGINX {filepath:s} konfliktas kaj unue devas esti forigita",
+ "service_started": "Servo '{service}' komenciĝis",
+ "port_already_opened": "Haveno {port} estas jam malfermita por {ip_version} rilatoj",
"upgrading_packages": "Ĝisdatigi pakojn…",
- "custom_app_url_required": "Vi devas provizi URL por altgradigi vian kutimon app {app:s}",
- "service_reload_failed": "Ne povis reŝargi la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "custom_app_url_required": "Vi devas provizi URL por altgradigi vian kutimon app {app}",
+ "service_reload_failed": "Ne povis reŝargi la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}",
"packages_upgrade_failed": "Ne povis ĝisdatigi ĉiujn pakojn",
- "hook_json_return_error": "Ne povis legi revenon de hoko {path:s}. Eraro: {msg:s}. Kruda enhavo: {raw_content}",
- "dyndns_cron_removed": "DynDNS cron-laboro forigita",
+ "hook_json_return_error": "Ne povis legi revenon de hoko {path}. Eraro: {msg}. Kruda enhavo: {raw_content}",
"dyndns_key_not_found": "DNS-ŝlosilo ne trovita por la domajno",
"tools_upgrade_regular_packages_failed": "Ne povis ĝisdatigi pakojn: {packages_list}",
- "service_start_failed": "Ne povis komenci la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
- "service_reloaded": "Servo '{service:s}' reŝargita",
+ "service_start_failed": "Ne povis komenci la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}",
+ "service_reloaded": "Servo '{service}' reŝargita",
"system_upgraded": "Sistemo ĝisdatigita",
"domain_deleted": "Domajno forigita",
- "certmanager_acme_not_configured_for_domain": "Atestilo por la domajno '{domain:s}' ne ŝajnas esti ĝuste instalita. Bonvolu ekzekuti 'cert-instali' por ĉi tiu regado unue.",
- "migration_description_0009_decouple_regenconf_from_services": "Malkonstruu la regen-konf-mekanismon de servoj",
+ "certmanager_acme_not_configured_for_domain": "Atestilo por la domajno '{domain}' ne ŝajnas esti ĝuste instalita. Bonvolu ekzekuti 'cert-instali' por ĉi tiu regado unue.",
"user_update_failed": "Ne povis ĝisdatigi uzanton {user}: {error}",
- "migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Lasu la SSH-agordon estu administrata de YunoHost (paŝo 2, manlibro)",
- "restore_confirm_yunohost_installed": "Ĉu vi vere volas restarigi jam instalitan sistemon? [{answers:s}]",
- "pattern_positive_number": "Devas esti pozitiva nombro",
- "certmanager_error_no_A_record": "Neniu DNS 'A' rekordo trovita por '{domain:s}'. Vi bezonas atentigi vian domajnan nomon al via maŝino por povi instali atestilon Lasu-Ĉifri. (Se vi scias, kion vi faras, uzu '--no-checks' por malŝalti tiujn ĉekojn.)",
+ "restore_confirm_yunohost_installed": "Ĉu vi vere volas restarigi jam instalitan sistemon? [{answers}]",
"update_apt_cache_failed": "Ne eblis ĝisdatigi la kaŝmemoron de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}",
"migrations_no_migrations_to_run": "Neniuj migradoj por funkcii",
- "executing_command": "Plenumanta komandon '{command:s}' …",
- "certmanager_attempt_to_renew_nonLE_cert": "La atestilo por la domajno '{domain:s}' ne estas elsendita de Let's Encrypt. Ne eblas renovigi ĝin aŭtomate!",
- "global_settings_setting_example_bool": "Ekzemplo bulea elekto",
+ "certmanager_attempt_to_renew_nonLE_cert": "La atestilo por la domajno '{domain}' ne estas elsendita de Let's Encrypt. Ne eblas renovigi ĝin aŭtomate!",
"domain_dyndns_already_subscribed": "Vi jam abonis DynDNS-domajnon",
"log_letsencrypt_cert_renew": "Renovigu '{}' Let's Encrypt atestilon",
- "migrate_tsig_start": "Detektita ŝlosila algoritmo nesufiĉa por TSIG-subskribo de la domajno '{domain}', komencanta migradon al la pli sekura HMAC-SHA-512",
- "ldap_init_failed_to_create_admin": "LDAP-iniciato ne povis krei administran uzanton",
"backup_output_directory_required": "Vi devas provizi elirejan dosierujon por la sekurkopio",
"tools_upgrade_cant_unhold_critical_packages": "Ne povis malŝalti kritikajn pakojn…",
"log_link_to_log": "Plena ŝtipo de ĉi tiu operacio: '{desc} '",
- "global_settings_cant_serialize_settings": "Ne eblis serialigi datumojn pri agordoj, motivo: {reason:s}",
+ "global_settings_cant_serialize_settings": "Ne eblis serialigi datumojn pri agordoj, motivo: {reason}",
"backup_running_hooks": "Kurado de apogaj hokoj …",
- "certmanager_domain_unknown": "Nekonata domajno '{domain:s}'",
"unexpected_error": "Io neatendita iris malbone: {error}",
"password_listed": "Ĉi tiu pasvorto estas inter la plej uzataj pasvortoj en la mondo. Bonvolu elekti ion pli unikan.",
- "migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Lasu la SSH-agordon estu administrata de YunoHost (paŝo 1, aŭtomata)",
- "migration_0009_not_needed": "Ĉi tiu migrado jam iel okazis ... (?) Saltado.",
"ssowat_conf_generated": "SSOwat-agordo generita",
- "migrate_tsig_wait": "Atendante tri minutojn por ke la servilo DynDNS enkalkulu la novan ŝlosilon …",
"log_remove_on_failed_restore": "Forigu '{}' post malsukcesa restarigo de rezerva ar archiveivo",
"dpkg_is_broken": "Vi ne povas fari ĉi tion nun ĉar dpkg/APT (la administrantoj pri pakaĵaj sistemoj) ŝajnas esti rompita stato ... Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.",
"certmanager_cert_signing_failed": "Ne povis subskribi la novan atestilon",
- "migration_description_0003_migrate_to_stretch": "Altgradigu la sistemon al Debian Stretch kaj YunoHost 3.0",
"log_tools_upgrade": "Ĝisdatigu sistemajn pakaĵojn",
"log_available_on_yunopaste": "Ĉi tiu protokolo nun haveblas per {url}",
- "certmanager_http_check_timeout": "Ekdifinita kiam servilo provis kontakti sin per HTTP per publika IP-adreso (domajno '{domain:s}' kun IP '{ip:s}'). Vi eble spertas haŭtoproblemon, aŭ la fajroŝirmilo / enkursigilo antaŭ via servilo miskonfiguras.",
"pattern_port_or_range": "Devas esti valida haveno-nombro (t.e. 0-65535) aŭ gamo da havenoj (t.e. 100:200)",
"migrations_loading_migration": "Ŝarĝante migradon {id}…",
"pattern_mailbox_quota": "Devas esti grandeco kun la sufikso b/k/M/G/T aŭ 0 por ne havi kvoton",
- "migration_0008_general_disclaimer": "Por plibonigi la sekurecon de via servilo, rekomendas lasi YunoHost administri la SSH-agordon. Via nuna SSH-aranĝo diferencas de la rekomendo. Se vi lasas YunoHost agordi ĝin, la maniero per kiu vi konektas al via servilo per SSH ŝanĝiĝos tiel:",
"user_deletion_failed": "Ne povis forigi uzanton {user}: {error}",
- "backup_with_no_backup_script_for_app": "La app '{app:s}' ne havas sekretan skripton. Ignorante.",
+ "backup_with_no_backup_script_for_app": "La app '{app}' ne havas sekretan skripton. Ignorante.",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' malakceptas! Bonvolu uzi anstataŭe 'yunohost tools regen-conf'.",
- "global_settings_key_doesnt_exists": "La ŝlosilo '{settings_key:s}' ne ekzistas en la tutmondaj agordoj, vi povas vidi ĉiujn disponeblajn klavojn per uzado de 'yunohost settings list'",
+ "global_settings_key_doesnt_exists": "La ŝlosilo '{settings_key}' ne ekzistas en la tutmondaj agordoj, vi povas vidi ĉiujn disponeblajn klavojn per uzado de 'yunohost settings list'",
"dyndns_no_domain_registered": "Neniu domajno registrita ĉe DynDNS",
- "dyndns_could_not_check_available": "Ne povis kontroli ĉu {domain:s} haveblas sur {provider:s}.",
- "global_settings_setting_example_enum": "Ekzemplo enum elekto",
- "hook_exec_not_terminated": "Skripto ne finiĝis ĝuste: {path:s}",
- "service_stopped": "Servo '{service:s}' ĉesis",
+ "dyndns_could_not_check_available": "Ne povis kontroli ĉu {domain} haveblas sur {provider}.",
+ "hook_exec_not_terminated": "Skripto ne finiĝis ĝuste: {path}",
+ "service_stopped": "Servo '{service}' ĉesis",
"restore_failed": "Ne povis restarigi sistemon",
- "confirm_app_install_danger": "Danĝero! Ĉi tiu apliko estas konata ankoraŭ eksperimenta (se ne eksplicite ne funkcias)! Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers:s}'",
+ "confirm_app_install_danger": "Danĝero! Ĉi tiu apliko estas konata ankoraŭ eksperimenta (se ne eksplicite ne funkcias)! Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers}'",
"log_operation_unit_unclosed_properly": "Operaciumo ne estis fermita ĝuste",
"upgrade_complete": "Ĝisdatigo kompleta",
"upnp_enabled": "UPnP ŝaltis",
"mailbox_used_space_dovecot_down": "La poŝta servo de Dovecot devas funkcii, se vi volas akcepti uzitan poŝtan keston",
- "restore_system_part_failed": "Ne povis restarigi la sisteman parton '{part:s}'",
- "service_stop_failed": "Ne povis maldaŭrigi la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
- "unbackup_app": "App '{app:s}' ne konserviĝos",
+ "restore_system_part_failed": "Ne povis restarigi la sisteman parton '{part}'",
+ "service_stop_failed": "Ne povis maldaŭrigi la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}",
+ "unbackup_app": "App '{app}' ne konserviĝos",
"updating_apt_cache": "Akirante haveblajn ĝisdatigojn por sistemaj pakoj…",
"tools_upgrade_at_least_one": "Bonvolu specifi '--apps' aŭ '--system'",
- "service_already_stopped": "La servo '{service:s}' jam ĉesis",
- "migration_0003_modified_files": "Bonvolu noti, ke la jenaj dosieroj estis trovitaj mane kaj modifitaj kaj povus esti anstataŭigitaj sekve de la ĝisdatigo: {manually_modified_files}",
+ "service_already_stopped": "La servo '{service}' jam ĉesis",
"tools_upgrade_cant_both": "Ne eblas ĝisdatigi ambaŭ sistemon kaj programojn samtempe",
"restore_extracting": "Eltirante bezonatajn dosierojn el la ar theivo…",
"upnp_port_open_failed": "Ne povis malfermi havenon per UPnP",
"log_app_upgrade": "Ĝisdatigu la aplikon '{}'",
"log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log share {name}' por akiri helpon",
- "migration_description_0002_migrate_to_tsig_sha256": "Plibonigu sekurecon de DynDNS TSIG-ĝisdatigoj per SHA-512 anstataŭ MD5",
- "port_already_closed": "Haveno {port:d} estas jam fermita por {ip_version:s} rilatoj",
- "hook_name_unknown": "Nekonata hoko-nomo '{name:s}'",
- "migration_0003_system_not_fully_up_to_date": "Via sistemo ne estas plene ĝisdata. Bonvolu plenumi regulan ĝisdatigon antaŭ ol ruli la migradon al Stretch.",
- "dyndns_could_not_check_provide": "Ne povis kontroli ĉu {provider:s} povas provizi {domain:s}.",
- "dyndns_cron_remove_failed": "Ne povis forigi la cron-laboron DynDNS ĉar: {error}",
+ "port_already_closed": "Haveno {port} estas jam fermita por {ip_version} rilatoj",
+ "hook_name_unknown": "Nekonata hoko-nomo '{name}'",
+ "dyndns_could_not_check_provide": "Ne povis kontroli ĉu {provider} povas provizi {domain}.",
"restore_nothings_done": "Nenio estis restarigita",
"log_tools_postinstall": "Afiŝu vian servilon YunoHost",
- "dyndns_unavailable": "La domajno '{domain:s}' ne haveblas.",
+ "dyndns_unavailable": "La domajno '{domain}' ne haveblas.",
"experimental_feature": "Averto: Ĉi tiu funkcio estas eksperimenta kaj ne konsiderata stabila, vi ne uzu ĝin krom se vi scias kion vi faras.",
"root_password_replaced_by_admin_password": "Via radika pasvorto estis anstataŭigita per via administra pasvorto.",
- "migration_description_0005_postgresql_9p4_to_9p6": "Migru datumbazojn de PostgreSQL 9.4 al 9.6",
- "migration_0008_root": "• Vi ne povos konekti kiel radiko per SSH. Anstataŭe vi uzu la administran uzanton;",
- "package_unknown": "Nekonata pako '{pkgname}'",
- "domain_unknown": "Nekonata domajno",
"global_settings_setting_security_password_user_strength": "Uzanto pasvorta forto",
- "restore_may_be_not_enough_disk_space": "Via sistemo ne ŝajnas havi sufiĉe da spaco (libera: {free_space:d} B, necesa spaco: {needed_space:d} B, sekureca marĝeno: {margin:d} B)",
+ "restore_may_be_not_enough_disk_space": "Via sistemo ne ŝajnas havi sufiĉe da spaco (libera: {free_space} B, necesa spaco: {needed_space} B, sekureca marĝeno: {margin} B)",
"log_corrupted_md_file": "La YAD-metadata dosiero asociita kun protokoloj estas damaĝita: '{md_file}\nEraro: {error} '",
"downloading": "Elŝutante …",
"user_deleted": "Uzanto forigita",
- "service_enable_failed": "Ne povis fari la servon '{service:s}' aŭtomate komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "service_enable_failed": "Ne povis fari la servon '{service}' aŭtomate komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs}",
"tools_upgrade_special_packages": "Nun ĝisdatigi 'specialajn' (rilatajn al yunohost)…",
"domains_available": "Haveblaj domajnoj:",
"dyndns_registered": "Registrita domajno DynDNS",
"service_description_fail2ban": "Protektas kontraŭ bruta forto kaj aliaj specoj de atakoj de la interreto",
- "file_does_not_exist": "La dosiero {path:s} ne ekzistas.",
+ "file_does_not_exist": "La dosiero {path} ne ekzistas.",
"yunohost_not_installed": "YunoHost ne estas ĝuste instalita. Bonvolu prilabori 'yunohost tools postinstall'",
- "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 estas instalita, sed ne postgresql 9.6‽ Io stranga eble okazis en via sistemo :(…",
"restore_removing_tmp_dir_failed": "Ne povis forigi malnovan provizoran dosierujon",
- "certmanager_cannot_read_cert": "Io malbona okazis, kiam mi provis malfermi aktualan atestilon por domajno {domain:s} (dosiero: {file:s}), kialo: {reason:s}",
- "service_removed": "Servo '{service:s}' forigita",
- "certmanager_hit_rate_limit": "Tro multaj atestiloj jam eldonitaj por ĉi tiu ĝusta aro de domajnoj {domain:s} antaŭ nelonge. Bonvolu reprovi poste. Vidu https://letsencrypt.org/docs/rate-limits/ por pliaj detaloj",
- "migration_0005_not_enough_space": "Disponigu sufiĉan spacon en {path} por ruli la migradon.",
+ "certmanager_cannot_read_cert": "Io malbona okazis, kiam mi provis malfermi aktualan atestilon por domajno {domain} (dosiero: {file}), kialo: {reason}",
+ "service_removed": "Servo '{service}' forigita",
+ "certmanager_hit_rate_limit": "Tro multaj atestiloj jam eldonitaj por ĉi tiu ĝusta aro de domajnoj {domain} antaŭ nelonge. Bonvolu reprovi poste. Vidu https://letsencrypt.org/docs/rate-limits/ por pliaj detaloj",
"pattern_firstname": "Devas esti valida antaŭnomo",
- "migration_description_0010_migrate_to_apps_json": "Forigu malvalorigitajn katalogajn programojn kaj uzu anstataŭe la novan unuigitan liston de \"apps.json\" (malaktuale anstataŭita per migrado 13)",
"domain_cert_gen_failed": "Ne povis generi atestilon",
"regenconf_file_kept_back": "La agorda dosiero '{conf}' estas atendita forigi per regen-conf (kategorio {category}), sed ĝi estis konservita.",
- "migrate_tsig_wait_4": "30 sekundoj …",
- "backup_with_no_restore_script_for_app": "La apliko \"{app:s}\" ne havas restarigan skripton, vi ne povos aŭtomate restarigi la sekurkopion de ĉi tiu apliko.",
+ "backup_with_no_restore_script_for_app": "La apliko \"{app}\" ne havas restarigan skripton, vi ne povos aŭtomate restarigi la sekurkopion de ĉi tiu apliko.",
"log_letsencrypt_cert_install": "Instalu atestilon Let's Encrypt sur '{}' regado",
"log_dyndns_update": "Ĝisdatigu la IP asociita kun via subdominio YunoHost '{}'",
"firewall_reload_failed": "Ne eblis reŝargi la firewall",
- "confirm_app_install_warning": "Averto: Ĉi tiu aplikaĵo povas funkcii, sed ne bone integras en YunoHost. Iuj funkcioj kiel ekzemple aliĝilo kaj sekurkopio / restarigo eble ne haveblos. Instali ĉiuokaze? [{answers:s}] ",
+ "confirm_app_install_warning": "Averto: Ĉi tiu aplikaĵo povas funkcii, sed ne bone integras en YunoHost. Iuj funkcioj kiel ekzemple aliĝilo kaj sekurkopio / restarigo eble ne haveblos. Instali ĉiuokaze? [{answers}] ",
"log_user_delete": "Forigi uzanton '{}'",
"dyndns_ip_updated": "Ĝisdatigis vian IP sur DynDNS",
"regenconf_up_to_date": "La agordo jam estas ĝisdatigita por kategorio '{category}'",
- "migration_0003_patching_sources_list": "Patching the sources.lists …",
"global_settings_setting_security_ssh_compatibility": "Kongruo vs sekureca kompromiso por la SSH-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)",
"migrations_need_to_accept_disclaimer": "Por funkciigi la migradon {id}, via devas akcepti la sekvan malakcepton:\n---\n{disclaimer}\n---\nSe vi akceptas funkcii la migradon, bonvolu rekonduki la komandon kun la opcio '--accept-disclaimer'.",
"regenconf_file_remove_failed": "Ne povis forigi la agordodosieron '{conf}'",
- "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path:s}'",
- "migration_0006_disclaimer": "YunoHost nun atendas, ke la pasvortoj de admin kaj radiko estos sinkronigitaj. Ĉi tiu migrado anstataŭigas vian radikan pasvorton kun la administran pasvorton.",
+ "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path}'",
"dyndns_ip_update_failed": "Ne povis ĝisdatigi IP-adreson al DynDNS",
- "migration_description_0004_php5_to_php7_pools": "Rekonfigu la PHP-naĝejojn por uzi PHP 7 anstataŭ 5",
"ssowat_conf_updated": "SSOwat-agordo ĝisdatigita",
"log_link_to_failed_log": "Ne povis plenumi la operacion '{desc}'. Bonvolu provizi la plenan protokolon de ĉi tiu operacio per alklakante ĉi tie por akiri helpon",
"user_home_creation_failed": "Ne povis krei dosierujon \"home\" por uzanto",
"pattern_backup_archive_name": "Devas esti valida dosiernomo kun maksimume 30 signoj, alfanombraj kaj -_. signoj nur",
"restore_cleaning_failed": "Ne eblis purigi la adresaron de provizora restarigo",
- "dyndns_registration_failed": "Ne povis registri DynDNS-domajnon: {error:s}",
- "migration_0003_not_jessie": "La nuna Debian-distribuo ne estas Jessie!",
- "user_unknown": "Nekonata uzanto: {user:s}",
+ "dyndns_registration_failed": "Ne povis registri DynDNS-domajnon: {error}",
+ "user_unknown": "Nekonata uzanto: {user}",
"migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations run`.",
- "migration_0008_warning": "Se vi komprenas tiujn avertojn kaj volas ke YunoHost preterlasu vian nunan agordon, faru la migradon. Alie, vi ankaŭ povas salti la migradon, kvankam ĝi ne rekomendas.",
- "certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain:s}'",
- "global_settings_reset_success": "Antaŭaj agordoj nun estas rezervitaj al {path:s}",
+ "certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain}'",
+ "global_settings_reset_success": "Antaŭaj agordoj nun estas rezervitaj al {path}",
"pattern_domain": "Devas esti valida domajna nomo (t.e. mia-domino.org)",
"dyndns_key_generating": "Generi DNS-ŝlosilon ... Eble daŭros iom da tempo.",
- "restore_running_app_script": "Restarigi la programon '{app:s}'…",
+ "restore_running_app_script": "Restarigi la programon '{app}'…",
"migrations_skip_migration": "Salti migradon {id}…",
"regenconf_file_removed": "Agordodosiero '{conf}' forigita",
"log_tools_shutdown": "Enŝaltu vian servilon",
"password_too_simple_3": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklon, pli malaltan kaj specialajn signojn",
- "migration_0003_general_warning": "Bonvolu noti, ke ĉi tiu migrado estas delikata operacio. La teamo de YunoHost faris sian plej bonan revizii kaj testi ĝin, sed la migrado eble ankoraŭ rompos partojn de la sistemo aŭ ĝiaj programoj.\n\nTial oni rekomendas al:\n - Elfari kopion de iuj kritikaj datumoj aŭ app. Pliaj informoj pri https://yunohost.org/backup;\n - Paciencu post lanĉo de la migrado: Depende de via interreta konekto kaj aparataro, eble daŭros kelkaj horoj ĝis ĉio ĝisdatigi.\n\nAldone, la haveno por SMTP, uzata de eksteraj retpoŝtaj klientoj (kiel Thunderbird aŭ K9-Mail) estis ŝanĝita de 465 (SSL / TLS) al 587 (STARTTLS). La malnova haveno (465) aŭtomate fermiĝos, kaj la nova haveno (587) malfermiĝos en la fajrejo. Vi kaj viaj uzantoj * devos adapti la agordon de viaj retpoŝtaj klientoj laŭe.",
- "global_settings_setting_example_int": "Ekzemple int elekto",
- "backup_output_symlink_dir_broken": "Via arkiva dosierujo '{path:s}' estas rompita ligilo. Eble vi forgesis restarigi aŭ munti aŭ enŝovi la stokadon, al kiu ĝi notas.",
+ "backup_output_symlink_dir_broken": "Via arkiva dosierujo '{path}' estas rompita ligilo. Eble vi forgesis restarigi aŭ munti aŭ enŝovi la stokadon, al kiu ĝi notas.",
"good_practices_about_admin_password": "Vi nun estas por difini novan administran pasvorton. La pasvorto devas esti almenaŭ 8 signojn - kvankam estas bone praktiki uzi pli longan pasvorton (t.e. pasfrazon) kaj/aŭ uzi variaĵon de signoj (majuskloj, minuskloj, ciferoj kaj specialaj signoj).",
- "certmanager_attempt_to_renew_valid_cert": "La atestilo por la domajno '{domain:s}' ne finiĝos! (Vi eble uzos --force se vi scias kion vi faras)",
+ "certmanager_attempt_to_renew_valid_cert": "La atestilo por la domajno '{domain}' ne finiĝos! (Vi eble uzos --force se vi scias kion vi faras)",
"restore_running_hooks": "Kurantaj restarigaj hokoj…",
"regenconf_pending_applying": "Aplikante pritraktata agordo por kategorio '{category}'…",
"service_description_dovecot": "Permesas al retpoŝtaj klientoj aliri / serĉi retpoŝton (per IMAP kaj POP3)",
"domain_dns_conf_is_just_a_recommendation": "Ĉi tiu komando montras al vi la *rekomenditan* agordon. Ĝi efektive ne agordas la DNS-agordon por vi. Via respondeco agordi vian DNS-zonon en via registristo laŭ ĉi tiu rekomendo.",
- "backup_php5_to_php7_migration_may_fail": "Ne povis konverti vian ar archiveivon por subteni PHP 7, vi eble ne povas restarigi viajn PHP-programojn (kialo: {error:s})",
"log_backup_restore_system": "Restarigi sistemon de rezerva arkivo",
"log_app_change_url": "Ŝanĝu la URL de la apliko '{}'",
- "service_already_started": "La servo '{service:s}' jam funkcias",
+ "service_already_started": "La servo '{service}' jam funkcias",
"global_settings_setting_security_password_admin_strength": "Admin pasvorta forto",
- "service_reload_or_restart_failed": "Ne povis reŝargi aŭ rekomenci la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "service_reload_or_restart_failed": "Ne povis reŝargi aŭ rekomenci la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}",
"migrations_list_conflict_pending_done": "Vi ne povas uzi ambaŭ '--previous' kaj '--done' samtempe.",
- "server_shutdown_confirm": "La servilo haltos tuj, ĉu vi certas? [{answers:s}]",
+ "server_shutdown_confirm": "La servilo haltos tuj, ĉu vi certas? [{answers}]",
"log_backup_restore_app": "Restarigu '{}' de rezerva ar archiveivo",
"log_does_exists": "Ne estas operacio kun la nomo '{log}', uzu 'yunohost log list' por vidi ĉiujn disponeblajn operaciojn",
- "service_add_failed": "Ne povis aldoni la servon '{service:s}'",
+ "service_add_failed": "Ne povis aldoni la servon '{service}'",
"pattern_password_app": "Bedaŭrinde, pasvortoj ne povas enhavi jenajn signojn: {forbidden_chars}",
"this_action_broke_dpkg": "Ĉi tiu ago rompis dpkg / APT (la administrantoj pri la paka sistemo) ... Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.",
"log_regen_conf": "Regeneri sistemajn agordojn '{}'",
- "restore_hook_unavailable": "Restariga skripto por '{part:s}' ne haveblas en via sistemo kaj ankaŭ ne en la ar theivo",
+ "restore_hook_unavailable": "Restariga skripto por '{part}' ne haveblas en via sistemo kaj ankaŭ ne en la ar theivo",
"log_dyndns_subscribe": "Aboni al YunoHost-subdominio '{}'",
"password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn",
- "migration_0003_main_upgrade": "Komencanta ĉefa ĝisdatigo …",
"regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita",
- "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}{name}'",
+ "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}'",
"global_settings_setting_security_nginx_compatibility": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)",
- "no_internet_connection": "La servilo ne estas konektita al la interreto",
- "migration_0008_dsa": "• La DSA-ŝlosilo estos malŝaltita. Tial vi eble bezonos nuligi spuran averton de via SSH-kliento kaj revizii la fingrospuron de via servilo;",
- "migration_0003_restoring_origin_nginx_conf": "Fileia dosiero /etc/nginx/nginx.conf estis iel redaktita. La migrado unue restarigos ĝin al sia originala stato ... La antaŭa dosiero estos havebla kiel {backup_dest}.",
- "migrate_tsig_end": "Migrado al HMAC-SHA-512 finiĝis",
"restore_complete": "Restarigita",
- "certmanager_couldnt_fetch_intermediate_cert": "Ekvilibrigita kiam vi provis ricevi interajn atestilojn de Let's Encrypt. Atestita instalado / renovigo nuligita - bonvolu reprovi poste.",
- "hook_exec_failed": "Ne povis funkcii skripto: {path:s}",
- "global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason:s}",
+ "hook_exec_failed": "Ne povis funkcii skripto: {path}",
+ "global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason}",
"user_created": "Uzanto kreita",
- "service_description_avahi-daemon": "Permesas al vi atingi vian servilon uzante 'yunohost.local' en via loka reto",
- "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain:s}! (Uzu --forte pretervidi)",
+ "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain}! (Uzu --forte pretervidi)",
"regenconf_updated": "Agordo ĝisdatigita por '{category}'",
"update_apt_cache_warning": "Io iris malbone dum la ĝisdatigo de la kaŝmemoro de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}",
"regenconf_dry_pending_applying": "Kontrolado de pritraktata agordo, kiu estus aplikita por kategorio '{category}'…",
"regenconf_file_copy_failed": "Ne povis kopii la novan agordodosieron '{new}' al '{conf}'",
- "global_settings_setting_example_string": "Ekzemple korda elekto",
- "restore_already_installed_app": "App kun la ID '{app:s}' estas jam instalita",
- "mail_domain_unknown": "Nevalida retadreso por domajno '{domain:s}'. Bonvolu uzi domajnon administritan de ĉi tiu servilo.",
+ "restore_already_installed_app": "App kun la ID '{app}' estas jam instalita",
+ "mail_domain_unknown": "Nevalida retadreso por domajno '{domain}'. Bonvolu uzi domajnon administritan de ĉi tiu servilo.",
"migrations_cant_reach_migration_file": "Ne povis aliri migrajn dosierojn ĉe la vojo '% s'",
"pattern_email": "Devas esti valida retpoŝta adreso (t.e. iu@ekzemple.com)",
- "mail_alias_remove_failed": "Ne povis forigi retpoŝton alias '{mail:s}'",
+ "mail_alias_remove_failed": "Ne povis forigi retpoŝton alias '{mail}'",
"regenconf_file_manually_removed": "La dosiero de agordo '{conf}' estis forigita permane, kaj ne estos kreita",
"domain_exists": "La domajno jam ekzistas",
- "migration_description_0001_change_cert_group_to_sslcert": "Ŝanĝu grupajn permesojn de 'metronomo' al 'ssl-cert'",
- "ldap_initialized": "LDAP inicializis",
- "migrate_tsig_not_needed": "Vi ne ŝajnas uzi DynDNS-domajnon, do neniu migrado necesas.",
- "certmanager_domain_cert_not_selfsigned": "La atestilo por domajno {domain:s} ne estas mem-subskribita. Ĉu vi certas, ke vi volas anstataŭigi ĝin? (Uzu '--force' por fari tion.)",
- "certmanager_unable_to_parse_self_CA_name": "Ne povis trapasi nomon de mem-subskribinta aŭtoritato (dosiero: {file:s})",
+ "certmanager_domain_cert_not_selfsigned": "La atestilo por domajno {domain} ne estas mem-subskribita. Ĉu vi certas, ke vi volas anstataŭigi ĝin? (Uzu '--force' por fari tion.)",
+ "certmanager_unable_to_parse_self_CA_name": "Ne povis trapasi nomon de mem-subskribinta aŭtoritato (dosiero: {file})",
"log_selfsigned_cert_install": "Instalu mem-subskribitan atestilon sur '{}' domajno",
"log_tools_reboot": "Reklamu vian servilon",
- "certmanager_cert_install_success": "Ni Ĉifru atestilon nun instalitan por la domajno '{domain:s}'",
- "global_settings_bad_choice_for_enum": "Malbona elekto por agordo {setting:s}, ricevita '{choice:s}', sed disponeblaj elektoj estas: {available_choices:s}",
+ "certmanager_cert_install_success": "Ni Ĉifru atestilon nun instalitan por la domajno '{domain}'",
+ "global_settings_bad_choice_for_enum": "Malbona elekto por agordo {setting}, ricevita '{choice}', sed disponeblaj elektoj estas: {available_choices}",
"server_shutdown": "La servilo haltos",
"log_tools_migrations_migrate_forward": "Kuru migradoj",
- "migration_0008_no_warning": "Supersalti vian SSH-agordon estu sekura, kvankam tio ne povas esti promesita! Ekfunkciu la migradon por superregi ĝin. Alie, vi ankaŭ povas salti la migradon, kvankam ĝi ne rekomendas.",
"regenconf_now_managed_by_yunohost": "La agorda dosiero '{conf}' nun estas administrata de YunoHost (kategorio {category}).",
- "server_reboot_confirm": "Ĉu la servilo rekomencos tuj, ĉu vi certas? [{answers:s}]",
+ "server_reboot_confirm": "Ĉu la servilo rekomencos tuj, ĉu vi certas? [{answers}]",
"log_app_install": "Instalu la aplikon '{}'",
"service_description_dnsmasq": "Traktas rezolucion de domajna nomo (DNS)",
- "global_settings_unknown_type": "Neatendita situacio, la agordo {setting:s} ŝajnas havi la tipon {unknown_type:s} sed ĝi ne estas tipo subtenata de la sistemo.",
- "migration_0003_problematic_apps_warning": "Bonvolu noti, ke la sekvaj eventuale problemaj instalitaj programoj estis detektitaj. Ŝajnas, ke tiuj ne estis instalitaj el aplika katalogo aŭ ne estas markitaj kiel \"funkciantaj\". Tial ne eblas garantii, ke ili ankoraŭ funkcios post la ĝisdatigo: {problematic_apps}",
+ "global_settings_unknown_type": "Neatendita situacio, la agordo {setting} ŝajnas havi la tipon {unknown_type} sed ĝi ne estas tipo subtenata de la sistemo.",
"domain_hostname_failed": "Ne povis agordi novan gastigilon. Ĉi tio eble kaŭzos problemon poste (eble bone).",
"server_reboot": "La servilo rekomenciĝos",
"regenconf_failed": "Ne povis regeneri la agordon por kategorio(j): {categories}",
"domain_uninstall_app_first": "Unu aŭ pluraj programoj estas instalitaj en ĉi tiu domajno. Bonvolu malinstali ilin antaŭ ol daŭrigi la domajnan forigon",
- "service_unknown": "Nekonata servo '{service:s}'",
- "migration_0003_start": "Komencante migradon al Stretch. La protokoloj haveblos en {logfile}.",
+ "service_unknown": "Nekonata servo '{service}'",
"domain_deletion_failed": "Ne eblas forigi domajnon {domain}: {error}",
"log_user_update": "Ĝisdatigu uzantinformojn de '{}'",
"user_creation_failed": "Ne povis krei uzanton {user}: {error}",
@@ -453,50 +366,44 @@
"done": "Farita",
"log_domain_remove": "Forigi domon '{}' de agordo de sistemo",
"hook_list_by_invalid": "Ĉi tiu posedaĵo ne povas esti uzata por listigi hokojn",
- "confirm_app_install_thirdparty": "Danĝero! Ĉi tiu apliko ne estas parto de la aplika katalogo de Yunohost. Instali triajn aplikojn povas kompromiti la integrecon kaj sekurecon de via sistemo. Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers:s}'",
+ "confirm_app_install_thirdparty": "Danĝero! Ĉi tiu apliko ne estas parto de la aplika katalogo de Yunohost. Instali triajn aplikojn povas kompromiti la integrecon kaj sekurecon de via sistemo. Vi probable ne devas instali ĝin krom se vi scias kion vi faras. NENIU SUBTENO estos provizita se ĉi tiu app ne funkcias aŭ rompas vian sistemon ... Se vi pretas riski ĉiuokaze, tajpu '{answers}'",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permesu uzon de (malaktuala) DSA-hostkey por la agordo de daemon SSH",
- "dyndns_domain_not_provided": "Provizanto DynDNS {provider:s} ne povas provizi domajnon {domain:s}.",
+ "dyndns_domain_not_provided": "Provizanto DynDNS {provider} ne povas provizi domajnon {domain}.",
"backup_unable_to_organize_files": "Ne povis uzi la rapidan metodon por organizi dosierojn en la ar archiveivo",
"password_too_simple_2": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklojn kaj minusklojn",
- "executing_script": "Plenumanta skripto '{script:s}' …",
- "service_cmd_exec_failed": "Ne povis plenumi la komandon '{command:s}'",
- "migration_0007_cancelled": "Ne povis plibonigi la manieron kiel via SSH-agordo estas administrita.",
- "migrate_tsig_failed": "Ne povis migri la DynDNS-domajnon '{domain}' al HMAC-SHA-512, ruliĝante. Eraro: {error_code}, {error}",
+ "service_cmd_exec_failed": "Ne povis plenumi la komandon '{command}'",
"pattern_lastname": "Devas esti valida familinomo",
- "service_enabled": "La servo '{service:s}' nun aŭtomate komenciĝos dum sistemaj botoj.",
- "certmanager_no_cert_file": "Ne povis legi la atestan dosieron por la domajno {domain:s} (dosiero: {file:s})",
- "migration_0008_port": "• Vi devos konekti uzante la havenon 22 anstataŭ via nuna kutimo SSH-haveno. Sentu vin libera reconfiguri ĝin;",
+ "service_enabled": "La servo '{service}' nun aŭtomate komenciĝos dum sistemaj botoj.",
+ "certmanager_no_cert_file": "Ne povis legi la atestan dosieron por la domajno {domain} (dosiero: {file})",
"domain_creation_failed": "Ne eblas krei domajnon {domain}: {error}",
- "certmanager_domain_http_not_working": "Ŝajnas ke la domajno {domain:s} ne atingeblas per HTTP. Kontrolu, ke via DNS kaj NGINX-agordo ĝustas",
- "domain_cannot_remove_main": "Vi ne povas forigi '{domain:s}' ĉar ĝi estas la ĉefa domajno, vi bezonas unue agordi alian domajnon kiel la ĉefan domajnon per uzado de 'yunohost domain main-domain -n ', jen la listo de kandidataj domajnoj. : {other_domains:s}",
- "service_reloaded_or_restarted": "La servo '{service:s}' estis reŝarĝita aŭ rekomencita",
+ "certmanager_domain_http_not_working": "Ŝajnas ke la domajno {domain} ne atingeblas per HTTP. Kontrolu, ke via DNS kaj NGINX-agordo ĝustas",
+ "domain_cannot_remove_main": "Vi ne povas forigi '{domain}' ĉar ĝi estas la ĉefa domajno, vi bezonas unue agordi alian domajnon kiel la ĉefan domajnon per uzado de 'yunohost domain main-domain -n ', jen la listo de kandidataj domajnoj. : {other_domains}",
+ "service_reloaded_or_restarted": "La servo '{service}' estis reŝarĝita aŭ rekomencita",
"log_domain_add": "Aldonu '{}' domajnon en sisteman agordon",
- "global_settings_bad_type_for_setting": "Malbona tipo por agordo {setting:s}, ricevita {received_type:s}, atendata {expected_type:s}",
+ "global_settings_bad_type_for_setting": "Malbona tipo por agordo {setting}, ricevita {received_type}, atendata {expected_type}",
"unlimit": "Neniu kvoto",
- "dyndns_cron_installed": "Kreita laboro DynDNS cron",
"system_username_exists": "Uzantnomo jam ekzistas en la listo de uzantoj de sistemo",
"firewall_reloaded": "Fajroŝirmilo reŝarĝis",
- "service_restarted": "Servo '{service:s}' rekomencis",
+ "service_restarted": "Servo '{service}' rekomencis",
"pattern_username": "Devas esti minuskulaj literoj kaj minuskloj nur",
"extracting": "Eltirante…",
- "restore_app_failed": "Ne povis restarigi la programon '{app:s}'",
+ "app_restore_failed": "Ne povis restarigi la programon '{app}': {error}",
"yunohost_configured": "YunoHost nun estas agordita",
- "certmanager_self_ca_conf_file_not_found": "Ne povis trovi agorddosieron por mem-subskriba aŭtoritato (dosiero: {file:s})",
+ "certmanager_self_ca_conf_file_not_found": "Ne povis trovi agorddosieron por mem-subskriba aŭtoritato (dosiero: {file})",
"log_app_remove": "Forigu la aplikon '{}'",
- "service_restart_failed": "Ne povis rekomenci la servon '{service:s}'\n\nLastatempaj servaj protokoloj: {logs:s}",
+ "service_restart_failed": "Ne povis rekomenci la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}",
"firewall_rules_cmd_failed": "Iuj komandoj pri fajroŝirmilo malsukcesis. Pliaj informoj en ensaluto.",
- "certmanager_certificate_fetching_or_enabling_failed": "Provante uzi la novan atestilon por {domain:s} ne funkciis …",
+ "certmanager_certificate_fetching_or_enabling_failed": "Provante uzi la novan atestilon por {domain} ne funkciis …",
"app_full_domain_unavailable": "Bedaŭrinde, ĉi tiu app devas esti instalita sur propra domajno, sed aliaj programoj jam estas instalitaj sur la domajno '{domain}'. Vi povus uzi subdominon dediĉitan al ĉi tiu app anstataŭe.",
- "migration_0011_slapd_config_will_be_overwritten": "Ŝajnas ke vi permane redaktis la slapd-agordon. Por ĉi tiu kritika migrado, YunoHost bezonas devigi la ĝisdatigon de la slapd-agordo. La originalaj dosieroj estos rezervitaj en {conf_backup_folder}.",
"group_cannot_edit_all_users": "La grupo 'all_users' ne povas esti redaktita permane. Ĝi estas speciala grupo celita enhavi ĉiujn uzantojn registritajn en YunoHost",
"group_cannot_edit_visitors": "La grupo 'vizitantoj' ne povas esti redaktita permane. Ĝi estas speciala grupo reprezentanta anonimajn vizitantojn",
"group_cannot_edit_primary_group": "La grupo '{group}' ne povas esti redaktita permane. Ĝi estas la primara grupo celita enhavi nur unu specifan uzanton.",
"log_permission_url": "Ĝisdatigu url-rilataj al permeso '{}'",
"permission_already_up_to_date": "La permeso ne estis ĝisdatigita ĉar la petoj pri aldono/forigo jam kongruas kun la aktuala stato.",
"permission_currently_allowed_for_all_users": "Ĉi tiu permeso estas nuntempe donita al ĉiuj uzantoj aldone al aliaj grupoj. Vi probable volas aŭ forigi la permeson \"all_users\" aŭ forigi la aliajn grupojn, kiujn ĝi nuntempe donas.",
- "app_install_failed": "Ne povis instali {app} : {error}",
+ "app_install_failed": "Ne povis instali {app}: {error}",
"app_install_script_failed": "Eraro okazis en la skripto de instalado de la app",
- "app_remove_after_failed_install": "Forigado de la app post la instala fiasko …",
+ "app_remove_after_failed_install": "Forigado de la programo post la instalado-fiasko ...",
"diagnosis_basesystem_host": "Servilo funkcias Debian {debian_version}",
"apps_catalog_init_success": "Aplikoj katalogsistemo inicializita !",
"apps_catalog_updating": "Ĝisdatigante katalogo de aplikoj …",
@@ -507,10 +414,8 @@
"diagnosis_basesystem_ynh_single_version": "{package} versio: {version} ({repo})",
"diagnosis_basesystem_ynh_main_version": "Servilo funkcias YunoHost {main_version} ({repo})",
"diagnosis_basesystem_ynh_inconsistent_versions": "Vi prizorgas malkonsekvencajn versiojn de la YunoHost-pakoj... plej probable pro malsukcesa aŭ parta ĝisdatigo.",
- "diagnosis_display_tip_web": "Vi povas iri al la sekcio Diagnozo (en la hejmekrano) por vidi la trovitajn problemojn.",
"diagnosis_cache_still_valid": "(La kaŝmemoro ankoraŭ validas por {category} diagnozo. Vi ankoraŭ ne diagnozas ĝin!)",
"diagnosis_cant_run_because_of_dep": "Ne eblas fari diagnozon por {category} dum estas gravaj problemoj rilataj al {dep}.",
- "diagnosis_display_tip_cli": "Vi povas aranĝi 'yunohost diagnosis show --issues' por aperigi la trovitajn problemojn.",
"diagnosis_failed_for_category": "Diagnozo malsukcesis por kategorio '{category}': {error}",
"app_upgrade_script_failed": "Eraro okazis en la skripto pri ĝisdatiga programo",
"diagnosis_diskusage_verylow": "Stokado {mountpoint}
(sur aparato {device}
) nur restas {free} ({free_percent}%) spaco restanta (el {total}). Vi vere konsideru purigi iom da spaco !",
@@ -519,7 +424,6 @@
"diagnosis_http_bad_status_code": "Ĝi aspektas kiel alia maŝino (eble via interreta enkursigilo) respondita anstataŭ via servilo.
1. La plej ofta kaŭzo por ĉi tiu afero estas, ke la haveno 80 (kaj 443) ne estas ĝuste senditaj al via servilo .
2. Pri pli kompleksaj agordoj: certigu, ke neniu fajroŝirmilo aŭ reverso-prokuro ne interbatalas.",
"main_domain_changed": "La ĉefa domajno estis ŝanĝita",
"yunohost_postinstall_end_tip": "La post-instalado finiĝis! Por fini vian agordon, bonvolu konsideri:\n - aldonado de unua uzanto tra la sekcio 'Uzantoj' de la retadreso (aŭ 'uzanto de yunohost kreu ' en komandlinio);\n - diagnozi eblajn problemojn per la sekcio 'Diagnozo' de la reteja administrado (aŭ 'diagnoza yunohost-ekzekuto' en komandlinio);\n - legante la partojn 'Finigi vian agordon' kaj 'Ekkoni Yunohost' en la administra dokumentado: https://yunohost.org/admindoc.",
- "migration_description_0014_remove_app_status_json": "Forigi heredajn dosierojn",
"diagnosis_ip_connected_ipv4": "La servilo estas konektita al la interreto per IPv4 !",
"diagnosis_ip_no_ipv4": "La servilo ne havas funkciantan IPv4.",
"diagnosis_ip_connected_ipv6": "La servilo estas konektita al la interreto per IPv6 !",
@@ -534,9 +438,6 @@
"diagnosis_swap_none": "La sistemo tute ne havas interŝanĝon. Vi devus pripensi aldoni almenaŭ {recommended} da interŝanĝo por eviti situaciojn en kiuj la sistemo restas sen memoro.",
"diagnosis_swap_notsomuch": "La sistemo havas nur {total}-interŝanĝon. Vi konsideru havi almenaŭ {recommended} por eviti situaciojn en kiuj la sistemo restas sen memoro.",
"diagnosis_regenconf_manually_modified_details": "Ĉi tio probable estas bona, se vi scias, kion vi faras! YunoHost ĉesigos ĝisdatigi ĉi tiun dosieron aŭtomate ... Sed atentu, ke YunoHost-ĝisdatigoj povus enhavi gravajn rekomendajn ŝanĝojn. Se vi volas, vi povas inspekti la diferencojn per yyunohost tools regen-conf {category} --dry-run --with-diff kaj devigi la reset al la rekomendita agordo per yunohost tools regen-conf {category} --force ",
- "diagnosis_regenconf_manually_modified_debian": "Agordodosiero {file} estis modifita permane kompare kun la defaŭlta Debian.",
- "diagnosis_regenconf_manually_modified_debian_details": "Ĉi tio probable estas bona, sed devas observi ĝin...",
- "diagnosis_security_all_good": "Neniu kritika sekureca vundebleco estis trovita.",
"diagnosis_security_vulnerable_to_meltdown": "Vi ŝajnas vundebla al la kritiko-vundebleco de Meltdown",
"diagnosis_no_cache": "Neniu diagnoza kaŝmemoro por kategorio '{category}'",
"diagnosis_ip_broken_dnsresolution": "Rezolucio pri domajna nomo rompiĝas pro iu kialo... Ĉu fajroŝirmilo blokas DNS-petojn ?",
@@ -547,14 +448,12 @@
"diagnosis_services_bad_status": "Servo {service} estas {status} :(",
"diagnosis_ram_low": "La sistemo havas {available} ({available_percent}%) RAM forlasita de {total}. Estu zorgema.",
"diagnosis_swap_ok": "La sistemo havas {total} da interŝanĝoj!",
- "diagnosis_mail_ougoing_port_25_ok": "Eliranta haveno 25 ne estas blokita kaj retpoŝto povas esti sendita al aliaj serviloj.",
"diagnosis_regenconf_allgood": "Ĉiuj agordaj dosieroj kongruas kun la rekomendita agordo!",
"diagnosis_regenconf_manually_modified": "Agordodosiero {file}
ŝajnas esti permane modifita.",
"diagnosis_description_ip": "Interreta konektebleco",
"diagnosis_description_dnsrecords": "Registroj DNS",
"diagnosis_description_services": "Servo kontrolas staton",
"diagnosis_description_systemresources": "Rimedaj sistemoj",
- "diagnosis_description_security": "Sekurecaj kontroloj",
"diagnosis_ports_could_not_diagnose": "Ne povis diagnozi, ĉu haveblaj havenoj de ekstere.",
"diagnosis_ports_could_not_diagnose_details": "Eraro: {error}",
"diagnosis_services_bad_status_tip": "Vi povas provi rekomenci la servon , kaj se ĝi ne funkcias, rigardu La servaj registroj en reteja (el la komandlinio, vi povas fari tion per yunohost service restart {service} kajyunohost service log {service} ).",
@@ -565,7 +464,6 @@
"log_domain_main_domain": "Faru de '{}' la ĉefa domajno",
"diagnosis_http_timeout": "Tempolimigita dum provado kontakti vian servilon de ekstere. Ĝi ŝajnas esti neatingebla.
1. La plej ofta kaŭzo por ĉi tiu afero estas, ke la haveno 80 (kaj 443) ne estas ĝuste senditaj al via servilo.
2. Vi ankaŭ devas certigi, ke la servo nginx funkcias
3. Pri pli kompleksaj agordoj: certigu, ke neniu fajroŝirmilo aŭ reverso-prokuro ne interbatalas.",
"diagnosis_http_connection_error": "Rilata eraro: ne povis konektiĝi al la petita domajno, tre probable ĝi estas neatingebla.",
- "migration_description_0013_futureproof_apps_catalog_system": "Migru al la nova katalogosistemo pri estontecaj programoj",
"diagnosis_ignored_issues": "(+ {nb_ignored} ignorataj aferoj))",
"diagnosis_found_errors": "Trovis {errors} signifa(j) afero(j) rilata al {category}!",
"diagnosis_found_errors_and_warnings": "Trovis {errors} signifaj problemo (j) (kaj {warnings} averto) rilataj al {category}!",
@@ -582,7 +480,7 @@
"diagnosis_http_could_not_diagnose_details": "Eraro: {error}",
"diagnosis_http_ok": "Domajno {domain} atingebla per HTTP de ekster la loka reto.",
"diagnosis_http_unreachable": "Domajno {domain} ŝajnas neatingebla per HTTP de ekster la loka reto.",
- "domain_cannot_remove_main_add_new_one": "Vi ne povas forigi '{domain:s}' ĉar ĝi estas la ĉefa domajno kaj via sola domajno, vi devas unue aldoni alian domajnon uzante ''yunohost domain add ', tiam agordi kiel ĉefan domajnon uzante 'yunohost domain main-domain -n ' kaj tiam vi povas forigi la domajnon' {domain:s} 'uzante' yunohost domain remove {domain:s} '.'",
+ "domain_cannot_remove_main_add_new_one": "Vi ne povas forigi '{domain}' ĉar ĝi estas la ĉefa domajno kaj via sola domajno, vi devas unue aldoni alian domajnon uzante ''yunohost domain add ', tiam agordi kiel ĉefan domajnon uzante 'yunohost domain main-domain -n ' kaj tiam vi povas forigi la domajnon' {domain} 'uzante' yunohost domain remove {domain} '.'",
"permission_require_account": "Permesilo {permission} nur havas sencon por uzantoj, kiuj havas konton, kaj tial ne rajtas esti ebligitaj por vizitantoj.",
"diagnosis_found_warnings": "Trovitaj {warnings} ero (j) kiuj povus esti plibonigitaj por {category}.",
"diagnosis_everything_ok": "Ĉio aspektas bone por {category}!",
@@ -590,19 +488,16 @@
"diagnosis_description_ports": "Ekspoziciaj havenoj",
"diagnosis_description_mail": "Retpoŝto",
"log_app_action_run": "Funkciigu agon de la apliko '{}'",
- "log_app_config_show_panel": "Montri la agordan panelon de la apliko '{}'",
- "log_app_config_apply": "Apliki agordon al la apliko '{}'",
"diagnosis_never_ran_yet": "Ŝajnas, ke ĉi tiu servilo estis instalita antaŭ nelonge kaj estas neniu diagnoza raporto por montri. Vi devas komenci kurante plenan diagnozon, ĉu de la retadministro aŭ uzante 'yunohost diagnosis run' el la komandlinio.",
- "certmanager_warning_subdomain_dns_record": "Subdominio '{subdomain:s}' ne solvas al la sama IP-adreso kiel '{domain:s}'. Iuj funkcioj ne estos haveblaj ĝis vi riparos ĉi tion kaj regeneros la atestilon.",
+ "certmanager_warning_subdomain_dns_record": "Subdominio '{subdomain}' ne solvas al la sama IP-adreso kiel '{domain}'. Iuj funkcioj ne estos haveblaj ĝis vi riparos ĉi tion kaj regeneros la atestilon.",
"diagnosis_basesystem_hardware": "Arkitekturo de servila aparataro estas {virt} {arch}",
- "diagnosis_basesystem_hardware_board": "Servilo-tabulo-modelo estas {model}",
"diagnosis_description_web": "Reta",
"domain_cannot_add_xmpp_upload": "Vi ne povas aldoni domajnojn per 'xmpp-upload'. Ĉi tiu speco de nomo estas rezervita por la XMPP-alŝuta funkcio integrita en YunoHost.",
"group_already_exist_on_system_but_removing_it": "Grupo {group} jam ekzistas en la sistemaj grupoj, sed YunoHost forigos ĝin …",
- "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Iuj provizantoj ne lasos vin malŝlosi elirantan havenon 25 ĉar ili ne zorgas pri Neta Neŭtraleco.
- Iuj el ili provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Amika privateco estas uzi VPN * kun dediĉita publika IP * por pretervidi ĉi tiun specon. de limoj. Vidu https://yunohost.org/#/vpn_avantage
- Vi ankaŭ povas konsideri ŝanĝi al pli neta neŭtraleco-amika provizanto",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Iuj provizantoj ne lasos vin malŝlosi elirantan havenon 25 ĉar ili ne zorgas pri Neta Neŭtraleco.
- Iuj el ili provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Amika privateco estas uzi VPN * kun dediĉita publika IP * por pretervidi ĉi tiun specon. de limoj. Vidu https://yunohost.org/#/vpn_avantage
- Vi ankaŭ povas konsideri ŝanĝi al pli neta neŭtraleco-amika provizanto",
"diagnosis_mail_fcrdns_nok_details": "Vi unue provu agordi la inversan DNS kun {ehlo_domain}
en via interreta enkursigilo aŭ en via retprovizanta interfaco. (Iuj gastigantaj provizantoj eble postulas, ke vi sendu al ili subtenan bileton por ĉi tio).",
- "diagnosis_mail_fcrdns_nok_alternatives_4": "Iuj provizantoj ne lasos vin agordi vian inversan DNS (aŭ ilia funkcio povus esti rompita ...). Se vi spertas problemojn pro tio, konsideru jenajn solvojn:
- Iuj ISP provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Interreta privateco estas uzi VPN * kun dediĉita publika IP * por preterpasi ĉi tiajn limojn. Vidu https://yunohost.org/#/vpn_avantage
- Finfine eblas ankaŭ ŝanĝo de provizanto",
- "diagnosis_display_tip": "Por vidi la trovitajn problemojn, vi povas iri al la sekcio pri Diagnozo de la reteja administrado, aŭ funkcii \"yunohost diagnosis show --issues\" el la komandlinio.",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Iuj provizantoj ne lasos vin agordi vian inversan DNS (aŭ ilia funkcio povus esti rompita ...). Se vi spertas problemojn pro tio, konsideru jenajn solvojn:
- Iuj ISP provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Interreta privateco estas uzi VPN * kun dediĉita publika IP * por preterpasi ĉi tiajn limojn. Vidu https://yunohost.org/#/vpn_avantage
- Finfine eblas ankaŭ ŝanĝo de provizanto",
+ "diagnosis_display_tip": "Por vidi la trovitajn problemojn, vi povas iri al la sekcio pri Diagnozo de la reteja administrado, aŭ funkcii \"yunohost diagnosis show --issues --human-readable\" el la komandlinio.",
"diagnosis_ip_global": "Tutmonda IP: {global}
",
"diagnosis_ip_local": "Loka IP: {local}
",
"diagnosis_dns_point_to_doc": "Bonvolu kontroli la dokumentaron ĉe https://yunohost.org/dns_config se vi bezonas helpon pri agordo de DNS-registroj.",
@@ -636,5 +531,19 @@
"diagnosis_http_partially_unreachable": "Domajno {domain} ŝajnas neatingebla per HTTP de ekster la loka reto en IPv {failed}, kvankam ĝi funkcias en IPv {passed}.",
"diagnosis_http_nginx_conf_not_up_to_date": "La nginx-agordo de ĉi tiu domajno ŝajnas esti modifita permane, kaj malhelpas YunoHost diagnozi ĉu ĝi atingeblas per HTTP.",
"diagnosis_http_nginx_conf_not_up_to_date_details": "Por solvi la situacion, inspektu la diferencon per la komandlinio per yunohost tools regen-conf nginx --dry-run --with-diff kaj se vi aranĝas, apliku la ŝanĝojn per yunohost tools regen-conf nginx --force .",
- "global_settings_setting_smtp_allow_ipv6": "Permesu la uzon de IPv6 por ricevi kaj sendi poŝton"
-}
+ "global_settings_setting_smtp_allow_ipv6": "Permesu la uzon de IPv6 por ricevi kaj sendi poŝton",
+ "backup_archive_corrupted": "I aspektas kiel la rezerva arkivo '{archive}' estas koruptita: {error}",
+ "backup_archive_cant_retrieve_info_json": "Ne povis ŝarĝi infos por arkivo '{archive}' ... la info.json ne povas esti reprenita (aŭ ne estas valida JSON).",
+ "ask_user_domain": "Domajno uzi por la retpoŝta adreso de la uzanto kaj XMPP-konto",
+ "app_packaging_format_not_supported": "Ĉi tiu programo ne povas esti instalita ĉar ĝia pakita formato ne estas subtenata de via Yunohost-versio. Vi probable devas konsideri ĝisdatigi vian sistemon.",
+ "app_restore_script_failed": "Eraro okazis ene de la App Restarigu Skripton",
+ "app_manifest_install_ask_is_public": "Ĉu ĉi tiu programo devas esti eksponita al anonimaj vizitantoj?",
+ "app_manifest_install_ask_admin": "Elektu administran uzanton por ĉi tiu programo",
+ "app_manifest_install_ask_password": "Elektu administradan pasvorton por ĉi tiu programo",
+ "app_manifest_install_ask_path": "Elektu la vojon, kie ĉi tiu programo devas esti instalita",
+ "app_manifest_install_ask_domain": "Elektu la domajnon, kie ĉi tiu programo devas esti instalita",
+ "app_label_deprecated": "Ĉi tiu komando estas malrekomendita! Bonvolu uzi la novan komandon 'yunohost user permission update' por administri la app etikedo.",
+ "app_argument_password_no_default": "Eraro dum analiza pasvorta argumento '{name}': pasvorta argumento ne povas havi defaŭltan valoron por sekureca kialo",
+ "additional_urls_already_removed": "Plia URL '{url}' jam forigita en la aldona URL por permeso '{permission}'",
+ "additional_urls_already_added": "Plia URL '{url}' jam aldonita en la aldona URL por permeso '{permission}'"
+}
\ No newline at end of file
diff --git a/locales/es.json b/locales/es.json
index cfcca071f..688db4546 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -1,51 +1,49 @@
{
- "action_invalid": "Acción no válida '{action:s} 1'",
+ "action_invalid": "Acción no válida '{action} 1'",
"admin_password": "Contraseña administrativa",
"admin_password_change_failed": "No se pudo cambiar la contraseña",
"admin_password_changed": "La contraseña de administración fue cambiada",
- "app_already_installed": "{app:s} ya está instalada",
- "app_argument_choice_invalid": "Use una de estas opciones «{choices:s}» para el argumento «{name:s}»",
- "app_argument_invalid": "Elija un valor válido para el argumento «{name:s}»: {error:s}",
- "app_argument_required": "Se requiere el argumento '{name:s} 7'",
+ "app_already_installed": "{app} ya está instalada",
+ "app_argument_choice_invalid": "Use una de estas opciones «{choices}» para el argumento «{name}»",
+ "app_argument_invalid": "Elija un valor válido para el argumento «{name}»: {error}",
+ "app_argument_required": "Se requiere el argumento '{name} 7'",
"app_extraction_failed": "No se pudieron extraer los archivos de instalación",
"app_id_invalid": "ID de la aplicación no válida",
"app_install_files_invalid": "Estos archivos no se pueden instalar",
"app_manifest_invalid": "Algo va mal con el manifiesto de la aplicación: {error}",
- "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada",
- "app_not_installed": "No se pudo encontrar «{app:s}» en la lista de aplicaciones instaladas: {all_apps}",
- "app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente",
- "app_removed": "Eliminado {app:s}",
+ "app_not_correctly_installed": "La aplicación {app} 8 parece estar incorrectamente instalada",
+ "app_not_installed": "No se pudo encontrar «{app}» en la lista de aplicaciones instaladas: {all_apps}",
+ "app_not_properly_removed": "La {app} 0 no ha sido desinstalada correctamente",
+ "app_removed": "Eliminado {app}",
"app_requirements_checking": "Comprobando los paquetes necesarios para {app}…",
"app_requirements_unmeet": "No se cumplen los requisitos para {app}, el paquete {pkgname} ({version}) debe ser {spec}",
"app_sources_fetch_failed": "No se pudieron obtener los archivos con el código fuente, ¿es el URL correcto?",
"app_unknown": "Aplicación desconocida",
"app_unsupported_remote_type": "Tipo remoto no soportado por la aplicación",
- "app_upgrade_failed": "No se pudo actualizar {app:s}: {error}",
- "app_upgraded": "Actualizado {app:s}",
- "ask_email": "Dirección de correo electrónico",
+ "app_upgrade_failed": "No se pudo actualizar {app}: {error}",
+ "app_upgraded": "Actualizado {app}",
"ask_firstname": "Nombre",
"ask_lastname": "Apellido",
"ask_main_domain": "Dominio principal",
"ask_new_admin_password": "Nueva contraseña administrativa",
"ask_password": "Contraseña",
- "backup_app_failed": "No se pudo respaldar «{app:s}»",
- "backup_archive_app_not_found": "No se pudo encontrar «{app:s}» en el archivo de respaldo",
+ "backup_app_failed": "No se pudo respaldar «{app}»",
+ "backup_archive_app_not_found": "No se pudo encontrar «{app}» en el archivo de respaldo",
"backup_archive_name_exists": "Ya existe un archivo de respaldo con este nombre.",
- "backup_archive_name_unknown": "Copia de seguridad local desconocida '{name:s}'",
+ "backup_archive_name_unknown": "Copia de seguridad local desconocida '{name}'",
"backup_archive_open_failed": "No se pudo abrir el archivo de respaldo",
"backup_cleaning_failed": "No se pudo limpiar la carpeta de respaldo temporal",
"backup_created": "Se ha creado la copia de seguridad",
"backup_creation_failed": "No se pudo crear el archivo de respaldo",
- "backup_delete_error": "No se pudo eliminar «{path:s}»",
+ "backup_delete_error": "No se pudo eliminar «{path}»",
"backup_deleted": "Eliminada la copia de seguridad",
- "backup_hook_unknown": "El gancho «{hook:s}» de la copia de seguridad es desconocido",
- "backup_invalid_archive": "Esto no es un archivo de respaldo",
+ "backup_hook_unknown": "El gancho «{hook}» de la copia de seguridad es desconocido",
"backup_nothings_done": "Nada que guardar",
"backup_output_directory_forbidden": "Elija un directorio de salida diferente. Las copias de seguridad no se pueden crear en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives subcarpetas",
"backup_output_directory_not_empty": "Debe elegir un directorio de salida vacío",
"backup_output_directory_required": "Debe proporcionar un directorio de salida para la copia de seguridad",
"backup_running_hooks": "Ejecutando los hooks de copia de respaldo...",
- "custom_app_url_required": "Debe proporcionar una URL para actualizar su aplicación personalizada {app:s}",
+ "custom_app_url_required": "Debe proporcionar una URL para actualizar su aplicación personalizada {app}",
"domain_cert_gen_failed": "No se pudo generar el certificado",
"domain_created": "Dominio creado",
"domain_creation_failed": "No se puede crear el dominio {domain}: {error}",
@@ -55,44 +53,34 @@
"domain_dyndns_root_unknown": "Dominio raíz de DynDNS desconocido",
"domain_exists": "El dominio ya existe",
"domain_uninstall_app_first": "Estas aplicaciones están todavía instaladas en tu dominio:\n{apps}\n\nPor favor desinstálalas utilizando yunohost app remove the_app_id
o cambialas a otro dominio usando yunohost app change-url the_app_id
antes de continuar con el borrado del dominio.",
- "domain_unknown": "Dominio desconocido",
"done": "Hecho.",
"downloading": "Descargando…",
- "dyndns_cron_installed": "Creado el trabajo de cron de DynDNS",
- "dyndns_cron_remove_failed": "No se pudo eliminar el trabajo de cron de DynDNS por: {error}",
- "dyndns_cron_removed": "Eliminado el trabajo de cron de DynDNS",
"dyndns_ip_update_failed": "No se pudo actualizar la dirección IP en DynDNS",
"dyndns_ip_updated": "Actualizada su IP en DynDNS",
"dyndns_key_generating": "Generando la clave del DNS. Esto podría tardar un rato.",
"dyndns_key_not_found": "No se ha encontrado la clave DNS para el dominio",
"dyndns_no_domain_registered": "Ningún dominio registrado con DynDNS",
"dyndns_registered": "Registrado dominio de DynDNS",
- "dyndns_registration_failed": "No se pudo registrar el dominio de DynDNS: {error:s}",
- "dyndns_unavailable": "El dominio «{domain:s}» no está disponible.",
- "executing_command": "Ejecutando la orden «{command:s}»…",
- "executing_script": "Ejecutando el guión «{script:s}»…",
+ "dyndns_registration_failed": "No se pudo registrar el dominio de DynDNS: {error}",
+ "dyndns_unavailable": "El dominio «{domain}» no está disponible.",
"extracting": "Extrayendo…",
- "field_invalid": "Campo no válido '{:s}'",
+ "field_invalid": "Campo no válido '{}'",
"firewall_reload_failed": "No se pudo recargar el cortafuegos",
"firewall_reloaded": "Cortafuegos recargado",
"firewall_rules_cmd_failed": "Algunos comandos para aplicar reglas del cortafuegos han fallado. Más información en el registro.",
- "hook_exec_failed": "No se pudo ejecutar el guión: {path:s}",
- "hook_exec_not_terminated": "El guión no terminó correctamente:{path:s}",
+ "hook_exec_failed": "No se pudo ejecutar el guión: {path}",
+ "hook_exec_not_terminated": "El guión no terminó correctamente:{path}",
"hook_list_by_invalid": "Esta propiedad no se puede usar para enumerar ganchos («hooks»)",
- "hook_name_unknown": "Nombre de hook desconocido '{name:s}'",
+ "hook_name_unknown": "Nombre de hook desconocido '{name}'",
"installation_complete": "Instalación finalizada",
- "installation_failed": "Algo ha ido mal con la instalación",
"ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción",
"iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción",
- "ldap_initialized": "Inicializado LDAP",
- "mail_alias_remove_failed": "No se pudo eliminar el alias de correo «{mail:s}»",
- "mail_domain_unknown": "Dirección de correo no válida para el dominio «{domain:s}». Use un dominio administrado por este servidor.",
- "mail_forward_remove_failed": "No se pudo eliminar el reenvío de correo «{mail:s}»",
+ "mail_alias_remove_failed": "No se pudo eliminar el alias de correo «{mail}»",
+ "mail_domain_unknown": "Dirección de correo no válida para el dominio «{domain}». Use un dominio administrado por este servidor.",
+ "mail_forward_remove_failed": "No se pudo eliminar el reenvío de correo «{mail}»",
"main_domain_change_failed": "No se pudo cambiar el dominio principal",
"main_domain_changed": "El dominio principal ha cambiado",
- "no_internet_connection": "El servidor no está conectado a Internet",
- "not_enough_disk_space": "No hay espacio libre suficiente en «{path:s}»",
- "package_unknown": "Paquete desconocido '{pkgname}'",
+ "not_enough_disk_space": "No hay espacio libre suficiente en «{path}»",
"packages_upgrade_failed": "No se pudieron actualizar todos los paquetes",
"pattern_backup_archive_name": "Debe ser un nombre de archivo válido con un máximo de 30 caracteres, solo se admiten caracteres alfanuméricos y los caracteres -_. (guiones y punto)",
"pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)",
@@ -102,44 +90,43 @@
"pattern_mailbox_quota": "Debe ser un tamaño con el sufijo «b/k/M/G/T» o «0» para no tener una cuota",
"pattern_password": "Debe contener al menos 3 caracteres",
"pattern_port_or_range": "Debe ser un número de puerto válido (es decir entre 0-65535) o un intervalo de puertos (por ejemplo 100:200)",
- "pattern_positive_number": "Deber ser un número positivo",
"pattern_username": "Solo puede contener caracteres alfanuméricos o el guión bajo",
- "port_already_closed": "El puerto {port:d} ya está cerrado para las conexiones {ip_version:s}",
- "port_already_opened": "El puerto {port:d} ya está abierto para las conexiones {ip_version:s}",
- "restore_already_installed_app": "Una aplicación con el ID «{app:s}» ya está instalada",
- "restore_app_failed": "No se pudo restaurar la aplicación «{app:s}»",
+ "port_already_closed": "El puerto {port} ya está cerrado para las conexiones {ip_version}",
+ "port_already_opened": "El puerto {port} ya está abierto para las conexiones {ip_version}",
+ "restore_already_installed_app": "Una aplicación con el ID «{app}» ya está instalada",
+ "app_restore_failed": "No se pudo restaurar la aplicación «{app}»: {error}",
"restore_cleaning_failed": "No se pudo limpiar el directorio temporal de restauración",
"restore_complete": "Restaurada",
- "restore_confirm_yunohost_installed": "¿Realmente desea restaurar un sistema ya instalado? [{answers:s}]",
+ "restore_confirm_yunohost_installed": "¿Realmente desea restaurar un sistema ya instalado? [{answers}]",
"restore_failed": "No se pudo restaurar el sistema",
- "restore_hook_unavailable": "El script de restauración para «{part:s}» no está disponible en su sistema y tampoco en el archivo",
+ "restore_hook_unavailable": "El script de restauración para «{part}» no está disponible en su sistema y tampoco en el archivo",
"restore_nothings_done": "No se ha restaurado nada",
- "restore_running_app_script": "Restaurando la aplicación «{app:s}»…",
+ "restore_running_app_script": "Restaurando la aplicación «{app}»…",
"restore_running_hooks": "Ejecutando los ganchos de restauración…",
- "service_add_failed": "No se pudo añadir el servicio «{service:s}»",
- "service_added": "Se agregó el servicio '{service:s}'",
- "service_already_started": "El servicio «{service:s}» ya está funcionando",
- "service_already_stopped": "El servicio «{service:s}» ya ha sido detenido",
- "service_cmd_exec_failed": "No se pudo ejecutar la orden «{command:s}»",
- "service_disable_failed": "No se pudo hacer que el servicio '{service:s}' no se iniciara en el arranque.\n\nRegistros de servicio recientes: {logs:s}",
- "service_disabled": "El servicio '{service:s}' ya no se iniciará cuando se inicie el sistema.",
- "service_enable_failed": "No se pudo hacer que el servicio '{service:s}' se inicie automáticamente en el arranque.\n\nRegistros de servicio recientes: {logs s}",
- "service_enabled": "El servicio '{service:s}' ahora se iniciará automáticamente durante el arranque del sistema.",
- "service_remove_failed": "No se pudo eliminar el servicio «{service:s}»",
- "service_removed": "Servicio '{service:s}' eliminado",
- "service_start_failed": "No se pudo iniciar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_started": "El servicio '{service:s}' comenzó",
- "service_stop_failed": "No se pudo detener el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_stopped": "Servicio '{service:s}' detenido",
- "service_unknown": "Servicio desconocido '{service:s}'",
+ "service_add_failed": "No se pudo añadir el servicio «{service}»",
+ "service_added": "Se agregó el servicio '{service}'",
+ "service_already_started": "El servicio «{service}» ya está funcionando",
+ "service_already_stopped": "El servicio «{service}» ya ha sido detenido",
+ "service_cmd_exec_failed": "No se pudo ejecutar la orden «{command}»",
+ "service_disable_failed": "No se pudo hacer que el servicio '{service}' no se iniciara en el arranque.\n\nRegistros de servicio recientes: {logs}",
+ "service_disabled": "El servicio '{service}' ya no se iniciará cuando se inicie el sistema.",
+ "service_enable_failed": "No se pudo hacer que el servicio '{service}' se inicie automáticamente en el arranque.\n\nRegistros de servicio recientes: {logs s}",
+ "service_enabled": "El servicio '{service}' ahora se iniciará automáticamente durante el arranque del sistema.",
+ "service_remove_failed": "No se pudo eliminar el servicio «{service}»",
+ "service_removed": "Servicio '{service}' eliminado",
+ "service_start_failed": "No se pudo iniciar el servicio «{service}»\n\nRegistro de servicios recientes:{logs}",
+ "service_started": "El servicio '{service}' comenzó",
+ "service_stop_failed": "No se pudo detener el servicio «{service}»\n\nRegistro de servicios recientes:{logs}",
+ "service_stopped": "Servicio '{service}' detenido",
+ "service_unknown": "Servicio desconocido '{service}'",
"ssowat_conf_generated": "Generada la configuración de SSOwat",
"ssowat_conf_updated": "Actualizada la configuración de SSOwat",
"system_upgraded": "Sistema actualizado",
"system_username_exists": "El nombre de usuario ya existe en la lista de usuarios del sistema",
- "unbackup_app": "La aplicación '{app:s}' no se guardará",
+ "unbackup_app": "La aplicación '{app}' no se guardará",
"unexpected_error": "Algo inesperado salió mal: {error}",
"unlimit": "Sin cuota",
- "unrestore_app": "La aplicación '{app:s}' no será restaurada",
+ "unrestore_app": "La aplicación '{app}' no será restaurada",
"updating_apt_cache": "Obteniendo las actualizaciones disponibles para los paquetes del sistema…",
"upgrade_complete": "Actualización finalizada",
"upgrading_packages": "Actualizando paquetes…",
@@ -152,75 +139,63 @@
"user_deleted": "Usuario eliminado",
"user_deletion_failed": "No se pudo eliminar el usuario {user}: {error}",
"user_home_creation_failed": "No se pudo crear la carpeta «home» para el usuario",
- "user_unknown": "Usuario desconocido: {user:s}",
+ "user_unknown": "Usuario desconocido: {user}",
"user_update_failed": "No se pudo actualizar el usuario {user}: {error}",
"user_updated": "Cambiada la información de usuario",
"yunohost_already_installed": "YunoHost ya está instalado",
- "yunohost_ca_creation_failed": "No se pudo crear la autoridad de certificación",
"yunohost_configured": "YunoHost está ahora configurado",
"yunohost_installing": "Instalando YunoHost…",
"yunohost_not_installed": "YunoHost no está correctamente instalado. Ejecute «yunohost tools postinstall»",
- "ldap_init_failed_to_create_admin": "La inicialización de LDAP no pudo crear el usuario «admin»",
"mailbox_used_space_dovecot_down": "El servicio de buzón Dovecot debe estar activo si desea recuperar el espacio usado del buzón",
- "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s}! (Use --force para omitir este mensaje)",
- "certmanager_domain_unknown": "Dominio desconocido «{domain:s}»",
- "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} no es un certificado autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use «--force» para hacerlo)",
- "certmanager_certificate_fetching_or_enabling_failed": "El intento de usar el nuevo certificado para {domain:s} no ha funcionado…",
- "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio «{domain:s}» no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!",
- "certmanager_attempt_to_renew_valid_cert": "¡El certificado para el dominio «{domain:s}» no está a punto de expirar! (Puede usar --force si sabe lo que está haciendo)",
- "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} a través de HTTP. Por favor compruebe en los diagnósticos la categoría 'Web'para más información. (Si sabe lo que está haciendo, utilice '--no-checks' para no realizar estas comprobaciones.)",
- "certmanager_error_no_A_record": "No se ha encontrado un registro DNS «A» para el dominio {domain:s}. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado de Let's Encrypt. (Si sabe lo que está haciendo, use «--no-checks» para desactivar esas comprobaciones.)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio '{domain:s}' es diferente de la IP de este servidor. Por favor comprueba los 'registros DNS' (básicos) la categoría de diagnósticos para mayor información. Si recientemente modificó su registro 'A', espere a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)",
- "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} (archivo: {file:s}), razón: {reason:s}",
- "certmanager_cert_install_success_selfsigned": "Instalado correctamente un certificado autofirmado para el dominio «{domain:s}»",
- "certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain:s}»",
- "certmanager_cert_renew_success": "Renovado correctamente el certificado de Let's Encrypt para el dominio «{domain:s}»",
- "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para este conjunto exacto de dominios {domain:s}. Pruebe de nuevo más tarde. Vea para más detalles https://letsencrypt.org/docs/rate-limits/",
+ "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain}! (Use --force para omitir este mensaje)",
+ "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain} no es un certificado autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use «--force» para hacerlo)",
+ "certmanager_certificate_fetching_or_enabling_failed": "El intento de usar el nuevo certificado para {domain} no ha funcionado…",
+ "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio «{domain}» no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!",
+ "certmanager_attempt_to_renew_valid_cert": "¡El certificado para el dominio «{domain}» no está a punto de expirar! (Puede usar --force si sabe lo que está haciendo)",
+ "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain} a través de HTTP. Por favor compruebe en los diagnósticos la categoría 'Web'para más información. (Si sabe lo que está haciendo, utilice '--no-checks' para no realizar estas comprobaciones.)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio '{domain}' es diferente de la IP de este servidor. Por favor comprueba los 'registros DNS' (básicos) la categoría de diagnósticos para mayor información. Si recientemente modificó su registro 'A', espere a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)",
+ "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain} (archivo: {file}), razón: {reason}",
+ "certmanager_cert_install_success_selfsigned": "Instalado correctamente un certificado autofirmado para el dominio «{domain}»",
+ "certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain}»",
+ "certmanager_cert_renew_success": "Renovado correctamente el certificado de Let's Encrypt para el dominio «{domain}»",
+ "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para este conjunto exacto de dominios {domain}. Pruebe de nuevo más tarde. Vea para más detalles https://letsencrypt.org/docs/rate-limits/",
"certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado",
- "certmanager_no_cert_file": "No se pudo leer el certificado para el dominio {domain:s} (archivo: {file:s})",
- "certmanager_conflicting_nginx_file": "No se pudo preparar el dominio para el desafío ACME: el archivo de configuración de NGINX {filepath:s} está en conflicto y debe ser eliminado primero",
- "domain_cannot_remove_main": "No puede eliminar '{domain:s}' ya que es el dominio principal, primero debe configurar otro dominio como el dominio principal usando 'yunohost domain main-domain -n '; Aquí está la lista de dominios candidatos: {other_domains:s}",
- "certmanager_self_ca_conf_file_not_found": "No se pudo encontrar el archivo de configuración para la autoridad de autofirma (archivo: {file:s})",
- "certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file:s})",
+ "certmanager_no_cert_file": "No se pudo leer el certificado para el dominio {domain} (archivo: {file})",
+ "domain_cannot_remove_main": "No puede eliminar '{domain}' ya que es el dominio principal, primero debe configurar otro dominio como el dominio principal usando 'yunohost domain main-domain -n '; Aquí está la lista de dominios candidatos: {other_domains}",
+ "certmanager_self_ca_conf_file_not_found": "No se pudo encontrar el archivo de configuración para la autoridad de autofirma (archivo: {file})",
+ "certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file})",
"domains_available": "Dominios disponibles:",
- "backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path:s})",
+ "backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path})",
"certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque su configuración de nginx no tiene el el código correcto... Por favor, asegurate que la configuración de nginx es correcta ejecutando en el terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.",
- "certmanager_http_check_timeout": "Tiempo de espera agotado cuando el servidor intentaba conectarse consigo mismo a través de HTTP usando una dirección IP pública (dominio «{domain:s}» con IP «{ip:s}»). Puede que esté experimentando un problema de redirección («hairpinning»), o que el cortafuegos o el enrutador de su servidor esté mal configurado.",
- "certmanager_couldnt_fetch_intermediate_cert": "Tiempo de espera agotado intentando obtener el certificado intermedio de Let's Encrypt. Cancelada la instalación o renovación del certificado. Vuelva a intentarlo más tarde.",
"domain_hostname_failed": "No se pudo establecer un nuevo nombre de anfitrión («hostname»). Esto podría causar problemas más tarde (no es seguro... podría ir bien).",
- "yunohost_ca_creation_success": "Creada la autoridad de certificación local.",
"app_already_installed_cant_change_url": "Esta aplicación ya está instalada. La URL no se puede cambiar solo con esta función. Marque `app changeurl` si está disponible.",
- "app_change_url_failed_nginx_reload": "No se pudo recargar NGINX. Esta es la salida de «nginx -t»:\n{nginx_errors:s}",
- "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain:s} {path:s}'), no se realizarán cambios.",
- "app_change_url_no_script": "La aplicación «{app_name:s}» aún no permite la modificación de URLs. Quizás debería actualizarla.",
- "app_change_url_success": "El URL de la aplicación {app:s} es ahora {domain:s} {path:s}",
- "app_location_unavailable": "Este URL o no está disponible o está en conflicto con otra(s) aplicación(es) instalada(s):\n{apps:s}",
- "app_already_up_to_date": "La aplicación {app:s} ya está actualizada",
+ "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain} {path}'), no se realizarán cambios.",
+ "app_change_url_no_script": "La aplicación «{app_name}» aún no permite la modificación de URLs. Quizás debería actualizarla.",
+ "app_change_url_success": "El URL de la aplicación {app} es ahora {domain} {path}",
+ "app_location_unavailable": "Este URL o no está disponible o está en conflicto con otra(s) aplicación(es) instalada(s):\n{apps}",
+ "app_already_up_to_date": "La aplicación {app} ya está actualizada",
"app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones",
"app_make_default_location_already_used": "No pudo hacer que la aplicación «{app}» sea la predeterminada en el dominio, «{domain}» ya está siendo usado por la aplicación «{other_app}»",
"app_upgrade_app_name": "Ahora actualizando {app}…",
"backup_abstract_method": "Este método de respaldo aún no se ha implementado",
- "backup_applying_method_borg": "Enviando todos los archivos para la copia de seguridad al repositorio de borg-backup…",
"backup_applying_method_copy": "Copiando todos los archivos en la copia de respaldo…",
- "backup_applying_method_custom": "Llamando al método de copia de seguridad personalizado «{method:s}»…",
+ "backup_applying_method_custom": "Llamando al método de copia de seguridad personalizado «{method}»…",
"backup_applying_method_tar": "Creando el archivo TAR de respaldo…",
- "backup_archive_system_part_not_available": "La parte del sistema «{part:s}» no está disponible en esta copia de seguridad",
- "backup_archive_writing_error": "No se pudieron añadir los archivos «{source:s}» (llamados en el archivo «{dest:s}») para ser respaldados en el archivo comprimido «{archive:s}»",
- "backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size:s}MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)",
- "backup_borg_not_implemented": "El método de respaldo de Borg aún no ha sido implementado",
+ "backup_archive_system_part_not_available": "La parte del sistema «{part}» no está disponible en esta copia de seguridad",
+ "backup_archive_writing_error": "No se pudieron añadir los archivos «{source}» (llamados en el archivo «{dest}») para ser respaldados en el archivo comprimido «{archive}»",
+ "backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size}MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)",
"backup_cant_mount_uncompress_archive": "No se pudo montar el archivo descomprimido como protegido contra escritura",
- "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar el archivo",
- "backup_couldnt_bind": "No se pudo enlazar {src:s} con {dest:s}.",
+ "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar el archivo",
+ "backup_couldnt_bind": "No se pudo enlazar {src} con {dest}.",
"backup_csv_addition_failed": "No se pudo añadir archivos para respaldar en el archivo CSV",
"backup_csv_creation_failed": "No se pudo crear el archivo CSV necesario para la restauración",
"backup_custom_mount_error": "El método de respaldo personalizado no pudo superar el paso «mount»",
"backup_no_uncompress_archive_dir": "No existe tal directorio de archivos sin comprimir",
- "backup_php5_to_php7_migration_may_fail": "No se pudo convertir su archivo para que sea compatible con PHP 7, puede que no pueda restaurar sus aplicaciones de PHP (motivo: {error:s})",
- "backup_system_part_failed": "No se pudo respaldar la parte del sistema «{part:s}»",
- "backup_with_no_backup_script_for_app": "La aplicación «{app:s}» no tiene un guión de respaldo. Omitiendo.",
- "backup_with_no_restore_script_for_app": "«{app:s}» no tiene un script de restauración, no podá restaurar automáticamente la copia de seguridad de esta aplicación.",
- "dyndns_could_not_check_provide": "No se pudo verificar si {provider:s} puede ofrecer {domain:s}.",
- "dyndns_domain_not_provided": "El proveedor de DynDNS {provider:s} no puede proporcionar el dominio {domain:s}.",
+ "backup_system_part_failed": "No se pudo respaldar la parte del sistema «{part}»",
+ "backup_with_no_backup_script_for_app": "La aplicación «{app}» no tiene un guión de respaldo. Omitiendo.",
+ "backup_with_no_restore_script_for_app": "«{app}» no tiene un script de restauración, no podá restaurar automáticamente la copia de seguridad de esta aplicación.",
+ "dyndns_could_not_check_provide": "No se pudo verificar si {provider} puede ofrecer {domain}.",
+ "dyndns_domain_not_provided": "El proveedor de DynDNS {provider} no puede proporcionar el dominio {domain}.",
"experimental_feature": "Aviso : esta funcionalidad es experimental y no se considera estable, no debería usarla a menos que sepa lo que está haciendo.",
"good_practices_about_user_password": "Ahora está a punto de definir una nueva contraseña de usuario. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).",
"password_listed": "Esta contraseña se encuentra entre las contraseñas más utilizadas en el mundo. Por favor, elija algo más único.",
@@ -228,7 +203,6 @@
"password_too_simple_2": "La contraseña tiene que ser de al menos 8 caracteres de longitud e incluir un número y caracteres en mayúsculas y minúsculas",
"password_too_simple_3": "La contraseña tiene que ser de al menos 8 caracteres de longitud e incluir un número, mayúsculas, minúsculas y caracteres especiales",
"password_too_simple_4": "La contraseña tiene que ser de al menos 12 caracteres de longitud e incluir un número, mayúsculas, minúsculas y caracteres especiales",
- "users_available": "Usuarios disponibles:",
"update_apt_cache_warning": "Algo fue mal durante la actualización de la caché de APT (gestor de paquetes de Debian). Aquí tiene un volcado de las líneas de sources.list que podría ayudarle a identificar las líneas problemáticas:\n{sourceslist}",
"update_apt_cache_failed": "No se pudo actualizar la caché de APT (gestor de paquetes de Debian). Aquí tiene un volcado de las líneas de sources.list que podría ayudarle a identificar las líneas problemáticas:\n{sourceslist}",
"tools_upgrade_special_packages_completed": "Actualización de paquetes de YunoHost completada.\nPulse [Intro] para regresar a la línea de órdenes",
@@ -241,12 +215,12 @@
"tools_upgrade_cant_both": "No se puede actualizar el sistema y las aplicaciones al mismo tiempo",
"tools_upgrade_at_least_one": "Especifique «--apps», o «--system»",
"this_action_broke_dpkg": "Esta acción rompió dpkg/APT(los gestores de paquetes del sistema)… Puede tratar de solucionar este problema conectando mediante SSH y ejecutando `sudo dpkg --configure -a`.",
- "service_reloaded_or_restarted": "El servicio '{service:s}' fue recargado o reiniciado",
- "service_reload_or_restart_failed": "No se pudo recargar o reiniciar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_restarted": "Servicio '{service:s}' reiniciado",
- "service_restart_failed": "No se pudo reiniciar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
- "service_reloaded": "Servicio '{service:s}' recargado",
- "service_reload_failed": "No se pudo recargar el servicio «{service:s}»\n\nRegistro de servicios recientes:{logs:s}",
+ "service_reloaded_or_restarted": "El servicio '{service}' fue recargado o reiniciado",
+ "service_reload_or_restart_failed": "No se pudo recargar o reiniciar el servicio «{service}»\n\nRegistro de servicios recientes:{logs}",
+ "service_restarted": "Servicio '{service}' reiniciado",
+ "service_restart_failed": "No se pudo reiniciar el servicio «{service}»\n\nRegistro de servicios recientes:{logs}",
+ "service_reloaded": "Servicio '{service}' recargado",
+ "service_reload_failed": "No se pudo recargar el servicio «{service}»\n\nRegistro de servicios recientes:{logs}",
"service_regen_conf_is_deprecated": "¡«yunohost service regen-conf» está obsoleto! Use «yunohost tools regen-conf» en su lugar.",
"service_description_yunohost-firewall": "Gestiona los puertos de conexiones abiertos y cerrados a los servicios",
"service_description_yunohost-api": "Gestiona las interacciones entre la interfaz web de YunoHost y el sistema",
@@ -255,26 +229,22 @@
"service_description_rspamd": "Filtra correo no deseado y otras características relacionadas con el correo",
"service_description_redis-server": "Una base de datos especializada usada para el acceso rápido de datos, cola de tareas y comunicación entre programas",
"service_description_postfix": "Usado para enviar y recibir correos",
- "service_description_php7.0-fpm": "Ejecuta aplicaciones escritas en PHP con NGINX",
- "service_description_nslcd": "Maneja la conexión del intérprete de órdenes («shell») de usuario de YunoHost",
"service_description_nginx": "Sirve o proporciona acceso a todos los sitios web alojados en su servidor",
"service_description_mysql": "Almacena los datos de la aplicación (base de datos SQL)",
"service_description_metronome": "Gestionar las cuentas XMPP de mensajería instantánea",
"service_description_fail2ban": "Protege contra ataques de fuerza bruta y otras clases de ataques desde Internet",
"service_description_dovecot": "Permite a los clientes de correo acceder/obtener correo (vía IMAP y POP3)",
"service_description_dnsmasq": "Maneja la resolución de nombres de dominio (DNS)",
- "service_description_avahi-daemon": "Permite acceder a su servidor usando «yunohost.local» en su red local",
- "server_reboot_confirm": "El servidor se reiniciará inmediatamente ¿está seguro? [{answers:s}]",
+ "server_reboot_confirm": "El servidor se reiniciará inmediatamente ¿está seguro? [{answers}]",
"server_reboot": "El servidor se reiniciará",
- "server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers:s}]",
+ "server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers}]",
"server_shutdown": "El servidor se apagará",
"root_password_replaced_by_admin_password": "Su contraseña de root ha sido sustituida por su contraseña de administración.",
"root_password_desynchronized": "La contraseña de administración ha sido cambiada pero ¡YunoHost no pudo propagar esto a la contraseña de root!",
- "restore_system_part_failed": "No se pudo restaurar la parte del sistema «{part:s}»",
+ "restore_system_part_failed": "No se pudo restaurar la parte del sistema «{part}»",
"restore_removing_tmp_dir_failed": "No se pudo eliminar un directorio temporal antiguo",
- "restore_not_enough_disk_space": "Espacio insuficiente (espacio: {free_space:d} B, espacio necesario: {needed_space:d} B, margen de seguridad: {margin:d} B)",
- "restore_mounting_archive": "Montando archivo en «{path:s}»",
- "restore_may_be_not_enough_disk_space": "Parece que su sistema no tiene suficiente espacio libre (libre: {free_space:d} B, espacio necesario: {needed_space:d} B, margen de seguridad: {margin:d} B)",
+ "restore_not_enough_disk_space": "Espacio insuficiente (espacio: {free_space} B, espacio necesario: {needed_space} B, margen de seguridad: {margin} B)",
+ "restore_may_be_not_enough_disk_space": "Parece que su sistema no tiene suficiente espacio libre (libre: {free_space} B, espacio necesario: {needed_space} B, margen de seguridad: {margin} B)",
"restore_extracting": "Extrayendo los archivos necesarios para el archivo…",
"regenconf_pending_applying": "Aplicando la configuración pendiente para la categoría «{category}»…",
"regenconf_failed": "No se pudo regenerar la configuración para la(s) categoría(s): {categories}",
@@ -291,16 +261,13 @@
"regenconf_file_kept_back": "Se espera que el archivo de configuración «{conf}» sea eliminado por regen-conf (categoría {category}) pero ha sido retenido.",
"regenconf_file_copy_failed": "No se pudo copiar el nuevo archivo de configuración «{new}» a «{conf}»",
"regenconf_file_backed_up": "Archivo de configuración «{conf}» respaldado en «{backup}»",
- "permission_update_nothing_to_do": "No hay permisos para actualizar",
- "permission_updated": "Actualizado el permiso «{permission:s}»",
- "permission_generated": "Actualizada la base de datos de permisos",
+ "permission_updated": "Actualizado el permiso «{permission}»",
"permission_update_failed": "No se pudo actualizar el permiso '{permission}': {error}",
- "permission_name_not_valid": "Elija un nombre de permiso permitido para «{permission:s}",
- "permission_not_found": "No se encontró el permiso «{permission:s}»",
+ "permission_not_found": "No se encontró el permiso «{permission}»",
"permission_deletion_failed": "No se pudo eliminar el permiso «{permission}»: {error}",
- "permission_deleted": "Eliminado el permiso «{permission:s}»",
+ "permission_deleted": "Eliminado el permiso «{permission}»",
"permission_creation_failed": "No se pudo crear el permiso «{permission}»: {error}",
- "permission_created": "Creado el permiso «{permission:s}»",
+ "permission_created": "Creado el permiso «{permission}»",
"permission_already_exist": "El permiso «{permission}» ya existe",
"pattern_password_app": "Las contraseñas no pueden incluir los siguientes caracteres: {forbidden_chars}",
"migrations_to_be_ran_manually": "La migración {id} hay que ejecutarla manualmente. Vaya a Herramientas → Migraciones en la página web de administración o ejecute `yunohost tools migrations run`.",
@@ -321,63 +288,8 @@
"migrations_dependencies_not_satisfied": "Ejecutar estas migraciones: «{dependencies_id}» antes de migrar {id}.",
"migrations_cant_reach_migration_file": "No se pudo acceder a los archivos de migración en la ruta «%s»",
"migrations_already_ran": "Esas migraciones ya se han realizado: {ids}",
- "migration_0011_update_LDAP_schema": "Actualizando el esquema de LDAP…",
- "migration_0011_update_LDAP_database": "Actualizando la base de datos de LDAP…",
- "migration_0011_rollback_success": "Sistema revertido.",
- "migration_0011_migration_failed_trying_to_rollback": "No se pudo migrar… intentando revertir el sistema.",
- "migration_0011_migrate_permission": "Migrando permisos desde la configuración de las aplicaciones a LDAP…",
- "migration_0011_LDAP_update_failed": "No se pudo actualizar LDAP. Error: {error:s}",
- "migration_0011_done": "Migración finalizada. Ahora puede gestionar los grupos de usuarios.",
- "migration_0011_create_group": "Creando un grupo para cada usuario…",
- "migration_0011_can_not_backup_before_migration": "El respaldo del sistema no se pudo completar antes de que la migración fallase. Error: {error:s}",
- "migration_0011_backup_before_migration": "Creando un respaldo de la base de datos de LDAP y de la configuración de las aplicaciones antes de la migración real.",
- "migration_0009_not_needed": "La migración ya ocurrió de algún modo… (?) Omitiendo.",
- "migration_0008_no_warning": "Sobre escribir su configuración SSH debería ser seguro ¡aunque esto no se puede prometer! Ejecute la migración para ignorarla. Por otra parte puede omitir la migración, aunque no se recomienda.",
- "migration_0008_warning": "Si entiende esos avisos y quiere que YunoHost ignore su configuración actual, ejecute la migración. Por otra parte puede omitir la migración, aunque no se recomienda.",
- "migration_0008_dsa": "• Se desactivará la clave DSA. Así que podría tener que anular un aviso espeluznante de su cliente SSH y volver a comprobar la huella de su servidor;",
- "migration_0008_root": "• No podrá conectarse como «root» a través de SSH. En su lugar debe usar el usuario «admin»;",
- "migration_0008_port": "• Tendrá que conectarse usando el puerto 22 en vez de su actual puerto SSH personalizado. No dude en reconfigurarlo;",
- "migration_0008_general_disclaimer": "Para mejorar la seguridad de su servidor, es recomendable permitir a YunoHost gestionar la configuración de SSH. Su actual configuración de SSH difiere de la recomendación. Si permite a YunoHost reconfigurarla, la manera en la que conecta con su servidor a través de SSH cambiará así:",
- "migration_0007_cannot_restart": "No se puede reiniciar SSH después de intentar cancelar la migración número 6.",
- "migration_0007_cancelled": "No se pudo mejorar el modo en el que se gestiona su configuración de SSH.",
- "migration_0006_disclaimer": "YunoHost espera ahora que las contraseñas de «admin» y «root» estén sincronizadas. Esta migración reemplaza su contraseña de «root» por la contraseña de «admin».",
- "migration_0005_not_enough_space": "Tenga suficiente espacio libre disponible en {path} para ejecutar la migración.",
- "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 está instalado pero no PostgreSQL 9.6. Algo raro podría haber ocurrido en su sistema:(…",
- "migration_0005_postgresql_94_not_installed": "PostgreSQL no estaba instalado en su sistema. Nada que hacer.",
- "migration_0003_modified_files": "Tenga en cuenta que se encontró que los siguientes archivos fueron modificados manualmente y podrían ser sobrescritos después de la actualización: {manually_modified_files}",
- "migration_0003_problematic_apps_warning": "Tenga en cuenta que se detectaron las siguientes aplicaciones instaladas posiblemente problemáticas. Parece que no se instalaron desde un catálogo de aplicaciones, o no se marcan como \"en funcionamiento\". En consecuencia, no se puede garantizar que seguirán funcionando después de la actualización: {problematic_apps}",
- "migration_0003_general_warning": "Tenga en cuenta que esta migración es una operación delicada. El equipo de YunoHost ha hecho todo lo posible para revisarla y probarla, pero la migración aún podría romper parte del sistema o de sus aplicaciones.\n\nPor lo tanto, se recomienda que:\n - Realice una copia de seguridad de cualquier dato crítico o aplicación. Más información en https://yunohost.org/backup;\n - Tenga paciencia tras iniciar la migración: dependiendo de su conexión a Internet y de su hardware, podría tardar unas cuantas horas hasta que todo se actualice.\n\nAdemás, el puerto para SMTP usado por los clientes de correo externos (como Thunderbird o K9-Mail) cambió de 465 (SSL/TLS) a 587 (STARTTLS). El antiguo puerto (465) se cerrará automáticamente y el nuevo puerto (587) se abrirá en el cortafuegos. Todos los usuarios *tendrán* que adaptar la configuración de sus clientes de correo por lo tanto.",
- "migration_0003_still_on_jessie_after_main_upgrade": "Algo fue mal durante la actualización principal: ⸘el sistema está aún en Jessie‽ Para investigar el problema, vea {log}:s…",
- "migration_0003_system_not_fully_up_to_date": "Su sistema no está totalmente actualizado. Realice una actualización normal antes de ejecutar la migración a Stretch.",
- "migration_0003_not_jessie": "¡La distribución de Debian actual no es Jessie!",
- "migration_0003_yunohost_upgrade": "Iniciando la actualización del paquete YunoHost… la actualización ocurrirá inmediatamente después de que la migración finalizará. Después de que la operación esté completada, podría tener que iniciar sesión en la página de administración de nuevo.",
- "migration_0003_restoring_origin_nginx_conf": "Su archivo /etc/nginx/nginx.conf ha sido editado. La migración lo devolverá a su estado original… El archivo anterior estará disponible como {backup_dest}.",
- "migration_0003_fail2ban_upgrade": "Iniciando la actualización de Fail2Ban…",
- "migration_0003_main_upgrade": "Iniciando la actualización principal…",
- "migration_0003_patching_sources_list": "Corrigiendo «sources.lists»…",
- "migration_0003_start": "Iniciando migración a Stretch. El registro estará disponible en {logfile}.",
- "migration_description_0012_postgresql_password_to_md5_authentication": "Forzar a la autentificación de PostgreSQL a usar MD5 para las conexiones locales",
- "migration_description_0011_setup_group_permission": "Configurar grupo de usuario y permisos para aplicaciones y servicios",
- "migration_description_0010_migrate_to_apps_json": "Elimine los catálogos de aplicaciones obsoletas y use la nueva lista unificada de 'apps.json' en su lugar (desactualizada, reemplazada por la migración 13)",
- "migration_description_0009_decouple_regenconf_from_services": "Separar el mecanismo «regen-conf» de los servicios",
- "migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Permitir que la configuración de SSH la gestione YunoHost (paso 2, manual)",
- "migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Permitir que la configuración de SSH la gestione YunoHost (paso 1, automático)",
- "migration_description_0006_sync_admin_and_root_passwords": "Sincronizar las contraseñas de «admin» y «root»",
- "migration_description_0005_postgresql_9p4_to_9p6": "Migrar las bases de datos de PostgreSQL 9.4 a 9.6",
- "migration_description_0004_php5_to_php7_pools": "Reconfigurar los «pools» de PHP para usar PHP 7 en vez de 5",
- "migration_description_0003_migrate_to_stretch": "Actualizar el sistema a Debian Stretch y YunoHost 3.0",
- "migration_description_0002_migrate_to_tsig_sha256": "Mejore la seguridad de las actualizaciones de TSIG de DynDNS usando SHA-512 en vez de MD5",
- "migration_description_0001_change_cert_group_to_sslcert": "Cambiar los permisos de grupo de certificados de «metronome» a «ssl-cert»",
- "migrate_tsig_not_needed": "Parece que no usa un dominio de DynDNS, así que no es necesario migrar.",
- "migrate_tsig_wait_4": "30 segundos…",
- "migrate_tsig_wait_3": "1 min. …",
- "migrate_tsig_wait_2": "2 min. …",
- "migrate_tsig_wait": "Esperando tres minutos para que el servidor de DynDNS tenga en cuenta la nueva clave…",
- "migrate_tsig_start": "Detectado algoritmo de clave insuficientemente seguro para la firma TSIG del dominio «{domain}», iniciando migración al más seguro HMAC-SHA-512",
- "migrate_tsig_failed": "No se pudo migrar el dominio de DynDNS «{domain}» a HMAC-SHA-512, revertiendo. Error: {error_code}, {error}",
- "migrate_tsig_end": "Terminada la migración a HMAC-SHA-512",
"mail_unavailable": "Esta dirección de correo está reservada y será asignada automáticamente al primer usuario",
- "mailbox_disabled": "Correo desactivado para usuario {user:s}",
+ "mailbox_disabled": "Correo desactivado para usuario {user}",
"log_tools_reboot": "Reiniciar el servidor",
"log_tools_shutdown": "Apagar el servidor",
"log_tools_upgrade": "Actualizar paquetes del sistema",
@@ -410,54 +322,48 @@
"log_does_exists": "No existe ningún registro de actividades con el nombre '{log}', ejecute 'yunohost log list' para ver todos los registros de actividades disponibles",
"log_help_to_get_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, comparta el registro completo de esta operación ejecutando la orden «yunohost log share {name}»",
"log_link_to_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, proporcione el registro completo de esta operación pulsando aquí",
- "log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log show {name}{name}»",
+ "log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log show {name}»",
"log_link_to_log": "Registro completo de esta operación: «{desc}»",
- "log_category_404": "La categoría de registro «{category}» no existe",
"log_corrupted_md_file": "El archivo de metadatos YAML asociado con el registro está dañado: «{md_file}\nError: {error}»",
- "hook_json_return_error": "No se pudo leer la respuesta del gancho {path:s}. Error: {msg:s}. Contenido sin procesar: {raw_content}",
+ "hook_json_return_error": "No se pudo leer la respuesta del gancho {path}. Error: {msg}. Contenido sin procesar: {raw_content}",
"group_update_failed": "No se pudo actualizar el grupo «{group}»: {error}",
"group_updated": "Grupo «{group}» actualizado",
- "group_unknown": "El grupo «{group:s}» es desconocido",
+ "group_unknown": "El grupo «{group}» es desconocido",
"group_deletion_failed": "No se pudo eliminar el grupo «{group}»: {error}",
"group_deleted": "Eliminado el grupo «{group}»",
"group_creation_failed": "No se pudo crear el grupo «{group}»: {error}",
"group_created": "Creado el grupo «{group}»",
"good_practices_about_admin_password": "Ahora está a punto de definir una nueva contraseña de administración. La contraseña debe tener al menos 8 caracteres, aunque es una buena práctica usar una contraseña más larga (es decir, una frase de contraseña) y / o usar una variación de caracteres (mayúsculas, minúsculas, dígitos y caracteres especiales).",
- "global_settings_unknown_type": "Situación imprevista, la configuración {setting:s} parece tener el tipo {unknown_type:s} pero no es un tipo compatible con el sistema.",
+ "global_settings_unknown_type": "Situación imprevista, la configuración {setting} parece tener el tipo {unknown_type} pero no es un tipo compatible con el sistema.",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir el uso de la llave (obsoleta) DSA para la configuración del demonio SSH",
- "global_settings_unknown_setting_from_settings_file": "Clave desconocida en la configuración: «{setting_key:s}», desechada y guardada en /etc/yunohost/settings-unknown.json",
+ "global_settings_unknown_setting_from_settings_file": "Clave desconocida en la configuración: «{setting_key}», desechada y guardada en /etc/yunohost/settings-unknown.json",
"global_settings_setting_security_postfix_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor Postfix. Afecta al cifrado (y otros aspectos relacionados con la seguridad)",
"global_settings_setting_security_ssh_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad)",
"global_settings_setting_security_password_user_strength": "Seguridad de la contraseña de usuario",
"global_settings_setting_security_password_admin_strength": "Seguridad de la contraseña del administrador",
"global_settings_setting_security_nginx_compatibility": "Compromiso entre compatibilidad y seguridad para el servidor web NGINX. Afecta al cifrado (y otros aspectos relacionados con la seguridad)",
- "global_settings_setting_example_string": "Ejemplo de opción de cadena",
- "global_settings_setting_example_int": "Ejemplo de opción «int»",
- "global_settings_setting_example_enum": "Ejemplo de opción «enum»",
- "global_settings_setting_example_bool": "Ejemplo de opción booleana",
- "global_settings_reset_success": "Respaldada la configuración previa en {path:s}",
- "global_settings_key_doesnt_exists": "La clave «{settings_key:s}» no existe en la configuración global, puede ver todas las claves disponibles ejecutando «yunohost settings list»",
- "global_settings_cant_write_settings": "No se pudo guardar el archivo de configuración, motivo: {reason:s}",
- "global_settings_cant_serialize_settings": "No se pudo seriar los datos de configuración, motivo: {reason:s}",
- "global_settings_cant_open_settings": "No se pudo abrir el archivo de configuración, motivo: {reason:s}",
- "global_settings_bad_type_for_setting": "Tipo erróneo para la configuración {setting:s}, obtuvo {received_type:s}, esperado {expected_type:s}",
- "global_settings_bad_choice_for_enum": "Opción errónea para la configuración {setting:s}, obtuvo «{choice:s}» pero las opciones disponibles son: {available_choices:s}",
- "file_does_not_exist": "El archivo {path:s} no existe.",
- "dyndns_could_not_check_available": "No se pudo comprobar si {domain:s} está disponible en {provider:s}.",
+ "global_settings_reset_success": "Respaldada la configuración previa en {path}",
+ "global_settings_key_doesnt_exists": "La clave «{settings_key}» no existe en la configuración global, puede ver todas las claves disponibles ejecutando «yunohost settings list»",
+ "global_settings_cant_write_settings": "No se pudo guardar el archivo de configuración, motivo: {reason}",
+ "global_settings_cant_serialize_settings": "No se pudo seriar los datos de configuración, motivo: {reason}",
+ "global_settings_cant_open_settings": "No se pudo abrir el archivo de configuración, motivo: {reason}",
+ "global_settings_bad_type_for_setting": "Tipo erróneo para la configuración {setting}, obtuvo {received_type}, esperado {expected_type}",
+ "global_settings_bad_choice_for_enum": "Opción errónea para la configuración {setting}, obtuvo «{choice}» pero las opciones disponibles son: {available_choices}",
+ "file_does_not_exist": "El archivo {path} no existe.",
+ "dyndns_could_not_check_available": "No se pudo comprobar si {domain} está disponible en {provider}.",
"domain_dns_conf_is_just_a_recommendation": "Esta orden muestra la configuración *recomendada*. No configura el DNS en realidad. Es su responsabilidad configurar la zona de DNS en su registrador según esta recomendación.",
"dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento ,parece que programa está usando el bloqueo de dpkg (el gestor de paquetes del sistema)",
"dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a`.",
- "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de Yunohost. La instalación de aplicaciones de terceros puede comprometer la integridad y la seguridad de su sistema. Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers:s}'",
- "confirm_app_install_danger": "¡PELIGRO! ¡Se sabe que esta aplicación sigue siendo experimental (si no explícitamente no funciona)! Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers:s}'",
- "confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers:s}] ",
+ "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de Yunohost. La instalación de aplicaciones de terceros puede comprometer la integridad y la seguridad de su sistema. Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers}'",
+ "confirm_app_install_danger": "¡PELIGRO! ¡Se sabe que esta aplicación sigue siendo experimental (si no explícitamente no funciona)! Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers}'",
+ "confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers}] ",
"backup_unable_to_organize_files": "No se pudo usar el método rápido de organización de los archivos en el archivo",
- "backup_permission": "Permiso de respaldo para {app:s}",
- "backup_output_symlink_dir_broken": "El directorio de su archivo «{path:s}» es un enlace simbólico roto. Tal vez olvidó (re)montarlo o conectarlo al medio de almacenamiento al que apunta.",
+ "backup_permission": "Permiso de respaldo para {app}",
+ "backup_output_symlink_dir_broken": "El directorio de su archivo «{path}» es un enlace simbólico roto. Tal vez olvidó (re)montarlo o conectarlo al medio de almacenamiento al que apunta.",
"backup_mount_archive_for_restore": "Preparando el archivo para restaurarlo…",
"backup_method_tar_finished": "Creado el archivo TAR de respaldo",
- "backup_method_custom_finished": "Terminado el método «{method:s}» de respaldo personalizado",
+ "backup_method_custom_finished": "Terminado el método «{method}» de respaldo personalizado",
"backup_method_copy_finished": "Terminada la copia de seguridad",
- "backup_method_borg_finished": "Terminado el respaldo en Borg",
"backup_custom_backup_error": "El método de respaldo personalizado no pudo superar el paso de «copia de seguridad»",
"backup_actually_backuping": "Creando un archivo de respaldo de los archivos obtenidos…",
"ask_new_path": "Nueva ruta",
@@ -486,7 +392,6 @@
"log_user_group_create": "Crear grupo «{}»",
"log_user_permission_update": "Actualizar los accesos para el permiso «{}»",
"log_user_permission_reset": "Restablecer permiso «{}»",
- "migration_0011_failed_to_remove_stale_object": "No se pudo eliminar el objeto obsoleto {dn}: {error}",
"permission_already_allowed": "El grupo «{group}» ya tiene el permiso «{permission}» activado",
"permission_already_disallowed": "El grupo '{group}' ya tiene el permiso '{permission}' deshabilitado",
"permission_cannot_remove_main": "No está permitido eliminar un permiso principal",
@@ -498,7 +403,6 @@
"group_cannot_edit_visitors": "El grupo «visitors» no se puede editar manualmente. Es un grupo especial que representa a los visitantes anónimos",
"group_cannot_edit_primary_group": "El grupo «{group}» no se puede editar manualmente. Es el grupo primario destinado a contener solo un usuario específico.",
"log_permission_url": "Actualizar la URL relacionada con el permiso «{}»",
- "migration_0011_slapd_config_will_be_overwritten": "Parece que ha editado manualmente la configuración de slapd. Para esta migración crítica, YunoHost necesita forzar la actualización de la configuración de slapd. Los archivos originales se respaldarán en {conf_backup_folder}.",
"permission_already_up_to_date": "El permiso no se ha actualizado porque las peticiones de incorporación o eliminación ya coinciden con el estado actual.",
"permission_currently_allowed_for_all_users": "Este permiso se concede actualmente a todos los usuarios además de los otros grupos. Probablemente quiere o eliminar el permiso de «all_users» o eliminar los otros grupos a los que está otorgado actualmente.",
"permission_require_account": "El permiso {permission} solo tiene sentido para usuarios con una cuenta y, por lo tanto, no se puede activar para visitantes.",
@@ -511,8 +415,6 @@
"diagnosis_failed_for_category": "Error de diagnóstico para la categoría '{category}': {error}",
"diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡No se volvera a comprobar de momento!)",
"diagnosis_found_errors_and_warnings": "¡Encontrado(s) error(es) significativo(s) {errors} (y aviso(s) {warnings}) relacionado(s) con {category}!",
- "diagnosis_display_tip_web": "Puede ir a la sección de diagnóstico (en la pantalla principal) para ver los problemas encontrados.",
- "diagnosis_display_tip_cli": "Puede ejecutar «yunohost diagnosis show --issues» para mostrar los problemas encontrados.",
"apps_catalog_init_success": "¡Sistema de catálogo de aplicaciones inicializado!",
"apps_catalog_updating": "Actualizando el catálogo de aplicaciones…",
"apps_catalog_failed_to_download": "No se puede descargar el catálogo de aplicaciones {apps_catalog}: {error}",
@@ -553,14 +455,10 @@
"diagnosis_ram_ok": "El sistema aun tiene {available} ({available_percent}%) de RAM de un total de {total}.",
"diagnosis_swap_none": "El sistema no tiene mas espacio de intercambio. Considera agregar por lo menos {recommended} de espacio de intercambio para evitar que el sistema se quede sin memoria.",
"diagnosis_swap_notsomuch": "Al sistema le queda solamente {total} de espacio de intercambio. Considera agregar al menos {recommended} para evitar que el sistema se quede sin memoria.",
- "diagnosis_mail_ougoing_port_25_ok": "El puerto de salida 25 no esta bloqueado y los correos electrónicos pueden ser enviados a otros servidores.",
"diagnosis_mail_outgoing_port_25_blocked": "El puerto de salida 25 parece estar bloqueado. Intenta desbloquearlo con el panel de configuración de tu proveedor de servicios de Internet (o proveedor de halbergue). Mientras tanto, el servidor no podrá enviar correos electrónicos a otros servidores.",
"diagnosis_regenconf_allgood": "Todos los archivos de configuración están en linea con la configuración recomendada!",
"diagnosis_regenconf_manually_modified": "El archivo de configuración {file} parece que ha sido modificado manualmente.",
"diagnosis_regenconf_manually_modified_details": "¡Esto probablemente esta BIEN si sabes lo que estás haciendo! YunoHost dejará de actualizar este fichero automáticamente... Pero ten en cuenta que las actualizaciones de YunoHost pueden contener importantes cambios que están recomendados. Si quieres puedes comprobar las diferencias mediante yunohost tools regen-conf {category} --dry-run --with-diff o puedes forzar el volver a las opciones recomendadas mediante el comando yunohost tools regen-conf {category} --force ",
- "diagnosis_regenconf_manually_modified_debian": "El archivos de configuración {file} fue modificado manualmente comparado con el valor predeterminado de Debian.",
- "diagnosis_regenconf_manually_modified_debian_details": "Esto este probablemente BIEN, pero igual no lo pierdas de vista...",
- "diagnosis_security_all_good": "Ninguna vulnerabilidad critica de seguridad fue encontrada.",
"diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable a el colapso de vulnerabilidad critica de seguridad",
"diagnosis_description_basesystem": "Sistema de base",
"diagnosis_description_ip": "Conectividad a Internet",
@@ -574,21 +472,15 @@
"diagnosis_ports_unreachable": "El puerto {port} no es accesible desde internet.",
"diagnosis_ports_could_not_diagnose": "No se puede comprobar si los puertos están accesibles desde el exterior.",
"diagnosis_ports_could_not_diagnose_details": "Error: {error}",
- "diagnosis_description_security": "Validación de seguridad",
"diagnosis_description_regenconf": "Configuraciones de sistema",
"diagnosis_description_mail": "Correo electrónico",
"diagnosis_description_web": "Web",
- "diagnosis_basesystem_hardware_board": "El modelo de placa del servidor es {model}",
"diagnosis_basesystem_hardware": "La arquitectura material del servidor es {virt} {arch}",
- "migration_description_0014_remove_app_status_json": "Supresión del archivo de aplicaciones heredado status.json",
- "migration_description_0013_futureproof_apps_catalog_system": "Migración hacia el nuevo sistema de catalogo de aplicación a prueba del futuro",
"log_domain_main_domain": "Hacer de '{}' el dominio principal",
- "log_app_config_apply": "Aplica la configuración de la aplicación '{}'",
- "log_app_config_show_panel": "Muestra el panel de configuración de la aplicación '{}'",
"log_app_action_run": "Inicializa la acción de la aplicación '{}'",
"group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en los grupos del sistema, pero YunoHost lo suprimirá …",
"global_settings_setting_pop3_enabled": "Habilita el protocolo POP3 para el servidor de correo electrónico",
- "domain_cannot_remove_main_add_new_one": "No se puede remover '{domain:s}' porque es su principal y único dominio. Primero debe agregar un nuevo dominio con la linea de comando 'yunohost domain add ', entonces configurarlo como dominio principal con 'yunohost domain main-domain -n ' y finalmente borrar el dominio '{domain:s}' con 'yunohost domain remove {domain:s}'.'",
+ "domain_cannot_remove_main_add_new_one": "No se puede remover '{domain}' porque es su principal y único dominio. Primero debe agregar un nuevo dominio con la linea de comando 'yunohost domain add ', entonces configurarlo como dominio principal con 'yunohost domain main-domain -n ' y finalmente borrar el dominio '{domain}' con 'yunohost domain remove {domain}'.'",
"diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.",
"diagnosis_unknown_categories": "Las siguientes categorías están desconocidas: {categories}",
"diagnosis_http_unreachable": "El dominio {domain} esta fuera de alcance desde internet y a través de HTTP.",
@@ -599,7 +491,7 @@
"diagnosis_http_could_not_diagnose": "No se pudo verificar si el dominio es accesible desde internet.",
"diagnosis_http_could_not_diagnose_details": "Error: {error}",
"diagnosis_ports_forwarding_tip": "Para solucionar este incidente, lo más seguro deberías configurar la redirección de los puertos en el router como se especifica en https://yunohost.org/isp_box_config",
- "certmanager_warning_subdomain_dns_record": "El subdominio '{subdomain:s}' no se resuelve en la misma dirección IP que '{domain:s}'. Algunas funciones no estarán disponibles hasta que solucione esto y regenere el certificado.",
+ "certmanager_warning_subdomain_dns_record": "El subdominio '{subdomain}' no se resuelve en la misma dirección IP que '{domain}'. Algunas funciones no estarán disponibles hasta que solucione esto y regenere el certificado.",
"domain_cannot_add_xmpp_upload": "No puede agregar dominios que comiencen con 'xmpp-upload'. Este tipo de nombre está reservado para la función de carga XMPP integrada en YunoHost.",
"yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, considere:\n - agregar un primer usuario a través de la sección 'Usuarios' del webadmin (o 'yunohost user create ' en la línea de comandos);\n - diagnostique problemas potenciales a través de la sección 'Diagnóstico' de webadmin (o 'ejecución de diagnóstico yunohost' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo a Yunohost' en la documentación del administrador: https://yunohost.org/admindoc.",
"diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.",
@@ -617,7 +509,7 @@
"diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por Yunohost. Si no es el caso, puede intentar forzar una actualización ejecutando yunohost dyndns update --force .",
"diagnosis_ip_local": "IP Local: {local}
",
"diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/#/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.",
- "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues' en la línea de comandos.",
+ "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues --human-readable' en la línea de comandos.",
"diagnosis_package_installed_from_sury_details": "Algunos paquetes fueron accidentalmente instalados de un repositorio de terceros llamado Sury. El equipo Yunohost ha mejorado la estrategia para manejar estos pquetes, pero es posible que algunas instalaciones con aplicaciones de PHP7.3 en Stretch puedan tener algunas inconsistencias. Para solucionar esta situación, debería intentar ejecutar el siguiente comando: {cmd_to_fix} ",
"diagnosis_package_installed_from_sury": "Algunos paquetes del sistema deberían ser devueltos a una versión anterior",
"certmanager_domain_not_diagnosed_yet": "Aún no hay resultado del diagnóstico para el dominio {domain}. Por favor ejecute el diagnóstico para las categorías 'Registros DNS' y 'Web' en la sección de diagnóstico para verificar si el dominio está listo para Let's Encrypt. (O si sabe lo que está haciendo, utilice '--no-checks' para deshabilitar esos chequeos.)",
@@ -645,7 +537,7 @@
"migration_description_0016_php70_to_php73_pools": "Migra el «pool» de ficheros php7.0-fpm a php7.3",
"migration_description_0015_migrate_to_buster": "Actualiza el sistema a Debian Buster y YunoHost 4.x",
"migrating_legacy_permission_settings": "Migrando los antiguos parámetros de permisos...",
- "invalid_regex": "Regex no valido: «{regex:s}»",
+ "invalid_regex": "Regex no valido: «{regex}»",
"global_settings_setting_backup_compress_tar_archives": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.",
"global_settings_setting_smtp_relay_password": "Clave de uso del SMTP",
"global_settings_setting_smtp_relay_user": "Cuenta de uso de SMTP",
@@ -671,7 +563,7 @@
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "El DNS inverso actual es: {rdns_domain}
Valor esperado: {ehlo_domain}
",
"diagnosis_mail_fcrdns_different_from_ehlo_domain": "La resolución de DNS inverso no está correctamente configurada mediante IPv{ipversion}. Algunos correos pueden fallar al ser enviados o pueden ser marcados como basura.",
"diagnosis_mail_fcrdns_nok_alternatives_6": "Algunos proveedores no permiten configurar el DNS inverso (o su funcionalidad puede estar rota...). Si tu DNS inverso está configurado correctamente para IPv4, puedes intentar deshabilitarlo para IPv6 cuando envies correos mediante el comando yunohost settings set smtp.allow_ipv6 -v off . Nota: esta solución quiere decir que no podrás enviar ni recibir correos con los pocos servidores que utilizan exclusivamente IPv6.",
- "diagnosis_mail_fcrdns_nok_alternatives_4": "Algunos proveedores no te permitirán que configures un DNS inverso (o puede que esta opción esté rota...). Si estás sufriendo problemas por este asunto, quizás te sirvan las siguientes soluciones:
- Algunos ISP proporcionan una alternativa mediante el uso de un relay de servidor de correo aunque esto implica que el relay podrá espiar tu tráfico de correo electrónico.
- Una solución amigable con la privacidad es utilizar una VPN con una *IP pública dedicada* para evitar este tipo de limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Quizás tu solución sea cambiar de proveedor de internet",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Algunos proveedores no te permitirán que configures un DNS inverso (o puede que esta opción esté rota...). Si estás sufriendo problemas por este asunto, quizás te sirvan las siguientes soluciones:
- Algunos ISP proporcionan una alternativa mediante el uso de un relay de servidor de correo aunque esto implica que el relay podrá espiar tu tráfico de correo electrónico.
- Una solución amigable con la privacidad es utilizar una VPN con una *IP pública dedicada* para evitar este tipo de limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Quizás tu solución sea cambiar de proveedor de internet",
"diagnosis_mail_fcrdns_nok_details": "Primero deberías intentar configurar el DNS inverso mediante {ehlo_domain}
en la interfaz de internet de tu router o en la de tu proveedor de internet. (Algunos proveedores de internet en ocasiones necesitan que les solicites un ticket de soporte para ello).",
"diagnosis_mail_fcrdns_dns_missing": "No hay definida ninguna DNS inversa mediante IPv{ipversion}. Algunos correos puede que fallen al enviarse o puede que se marquen como basura.",
"diagnosis_mail_fcrdns_ok": "¡Las DNS inversas están bien configuradas!",
@@ -684,9 +576,9 @@
"diagnosis_mail_ehlo_unreachable_details": "No pudo abrirse la conexión en el puerto 25 de tu servidor mediante IPv{ipversion}. Parece que no se puede contactar.
1. La causa más común en estos casos suele ser que el puerto 25 no está correctamente redireccionado a tu servidor.
2. También deberías asegurarte que el servicio postfix está en marcha.
3. En casos más complejos: asegurate que no estén interfiriendo ni el firewall ni el reverse-proxy.",
"diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos",
"diagnosis_mail_ehlo_ok": "¡El servidor de correo SMTP puede contactarse desde el exterior por lo que puede recibir correos!",
- "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu trafico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de inteernet a uno más amable con la Neutralidad de la Red",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu trafico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de inteernet a uno más amable con la Neutralidad de la Red",
"diagnosis_backports_in_sources_list": "Parece que apt (el gestor de paquetes) está configurado para usar el repositorio backports. A menos que realmente sepas lo que estás haciendo, desaconsejamos absolutamente instalar paquetes desde backports, ya que pueden provocar comportamientos intestables o conflictos en el sistema.",
"diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}",
- "additional_urls_already_removed": "La URL adicional «{url:s}» ya se ha eliminado para el permiso «{permission:s}»",
- "additional_urls_already_added": "La URL adicional «{url:s}» ya se ha añadido para el permiso «{permission:s}»"
-}
+ "additional_urls_already_removed": "La URL adicional «{url}» ya se ha eliminado para el permiso «{permission}»",
+ "additional_urls_already_added": "La URL adicional «{url}» ya se ha añadido para el permiso «{permission}»"
+}
\ No newline at end of file
diff --git a/locales/fa.json b/locales/fa.json
new file mode 100644
index 000000000..f566fed90
--- /dev/null
+++ b/locales/fa.json
@@ -0,0 +1,640 @@
+{
+ "action_invalid": "اقدام نامعتبر '{action}'",
+ "aborting": "رها کردن.",
+ "app_argument_required": "استدلال '{name}' الزامی است",
+ "app_argument_password_no_default": "خطا هنگام تجزیه گذرواژه '{name}': به دلایل امنیتی استدلال رمز عبور نمی تواند مقدار پیش فرض داشته باشد",
+ "app_argument_invalid": "یک مقدار معتبر انتخاب کنید برای استدلال '{name}':{error}",
+ "app_argument_choice_invalid": "برای آرگومان '{name}' از یکی از این گزینه ها '{choices}' استفاده کنید",
+ "app_already_up_to_date": "{app} در حال حاضر به روز است",
+ "app_already_installed_cant_change_url": "این برنامه قبلاً نصب شده است. URL فقط با این عملکرد قابل تغییر نیست. در صورت موجود بودن برنامه `app changeurl` را بررسی کنید.",
+ "app_already_installed": "{app} قبلاً نصب شده است",
+ "app_action_broke_system": "این اقدام به نظر می رسد سرویس های مهمی را خراب کرده است: {services}",
+ "app_action_cannot_be_ran_because_required_services_down": "برای اجرای این عملیات سرویس هایی که مورد نیازاند و باید اجرا شوند: {services}. سعی کنید آنها را مجدداً راه اندازی کنید (و علت خرابی احتمالی آنها را بررسی کنید).",
+ "already_up_to_date": "کاری برای انجام دادن نیست. همه چیز در حال حاضر به روز است.",
+ "admin_password_too_long": "لطفاً گذرواژه ای کوتاهتر از 127 کاراکتر انتخاب کنید",
+ "admin_password_changed": "رمز مدیریت تغییر کرد",
+ "admin_password_change_failed": "تغییر رمز امکان پذیر نیست",
+ "admin_password": "رمز عبور مدیریت",
+ "additional_urls_already_removed": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}'حذف شده است",
+ "additional_urls_already_added": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}' اضافه شده است",
+ "diagnosis_diskusage_low": "ذخیره سازی {mountpoint}
(روی دستگاه {device}
) فقط {free} ({free_percent}%) فضا باقی مانده(از {total}). مراقب باشید.",
+ "diagnosis_diskusage_verylow": "ذخیره سازی {mountpoint}
(روی دستگاه {device}
) فقط {free} ({free_percent}%) فضا باقی مانده (از {total}). شما واقعاً باید پاکسازی فضای ذخیره ساز را در نظر بگیرید!",
+ "diagnosis_services_bad_status_tip": "می توانید سعی کنید سرویس را راه اندازی مجدد کنید، و اگر کار نمی کند ، نگاهی داشته باشید بهسرویس در webadmin ثبت می شود (از خط فرمان ، می توانید این کار را انجام دهید با yunohost service restart {service} و yunohost service log {service} ).",
+ "diagnosis_services_bad_status": "سرویس {service} {status} است :(",
+ "diagnosis_services_conf_broken": "پیکربندی سرویس {service} خراب است!",
+ "diagnosis_services_running": "سرویس {service} در حال اجرا است!",
+ "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_dns_specialusedomain": "دامنه {domain} بر اساس یک دامنه سطح بالا (TLD) مخصوص استفاده است و بنابراین انتظار نمی رود که دارای سوابق DNS واقعی باشد.",
+ "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost dyndns update --force .",
+ "diagnosis_dns_point_to_doc": "لطفاً اسناد را در https://yunohost.org/dns_config برسی و مطالعه کنید، اگر در مورد پیکربندی سوابق DNS به کمک نیاز دارید.",
+ "diagnosis_dns_discrepancy": "به نظر می رسد پرونده DNS زیر از پیکربندی توصیه شده پیروی نمی کند:
نوع: {type}
نام: {name}
ارزش فعلی: {current}
مقدار مورد انتظار: {value}
",
+ "diagnosis_dns_missing_record": "با توجه به پیکربندی DNS توصیه شده ، باید یک رکورد DNS با اطلاعات زیر اضافه کنید.
نوع: {type}
نام: {name}
ارزش: {value}
",
+ "diagnosis_dns_bad_conf": "برخی از سوابق DNS برای دامنه {domain} (دسته {category}) وجود ندارد یا نادرست است",
+ "diagnosis_dns_good_conf": "سوابق DNS برای دامنه {domain} (دسته {category}) به درستی پیکربندی شده است",
+ "diagnosis_ip_weird_resolvconf_details": "پرونده /etc/resolv.conf
باید یک پیوند همراه برای /etc/resolvconf/run/resolv.conf
خود اشاره می کند به 127.0.0.1
(dnsmasq). اگر می خواهید راه حل های DNS را به صورت دستی پیکربندی کنید ، لطفاً ویرایش کنید /etc/resolv.dnsmasq.conf
.",
+ "diagnosis_ip_weird_resolvconf": "اینطور که پیداست تفکیک پذیری DNS کار می کند ، اما به نظر می رسد از سفارشی استفاده می کنید /etc/resolv.conf
.",
+ "diagnosis_ip_broken_resolvconf": "به نظر می رسد تفکیک پذیری نام دامنه در سرور شما شکسته شده است ، که به نظر می رسد مربوط به /etc/resolv.conf
و اشاره نکردن به 127.0.0.1
میباشد.",
+ "diagnosis_ip_broken_dnsresolution": "به نظر می رسد تفکیک پذیری نام دامنه به دلایلی خراب شده است... آیا فایروال درخواست های DNS را مسدود می کند؟",
+ "diagnosis_ip_dnsresolution_working": "تفکیک پذیری نام دامنه کار می کند!",
+ "diagnosis_ip_not_connected_at_all": "به نظر می رسد سرور اصلا به اینترنت متصل نیست !؟",
+ "diagnosis_ip_local": "IP محلی: {local}
",
+ "diagnosis_ip_global": "IP جهانی: {global}
",
+ "diagnosis_ip_no_ipv6_tip": "داشتن یک IPv6 فعال برای کار سرور شما اجباری نیست ، اما برای سلامت اینترنت به طور کلی بهتر است. IPv6 معمولاً باید در صورت موجود بودن توسط سیستم یا ارائه دهنده اینترنت شما به طور خودکار پیکربندی شود. در غیر این صورت ، ممکن است لازم باشد چند مورد را به صورت دستی پیکربندی کنید ، همانطور که در اسناد اینجا توضیح داده شده است: https://yunohost.org/#/ipv6.اگر نمی توانید 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": "در حالی که مشکلات مهمی در ارتباط با {dep} وجود دارد ، نمی توان عیب یابی را برای {category} اجرا کرد.",
+ "diagnosis_cache_still_valid": "(حافظه پنهان هنوز برای عیب یابی {category} معتبر است. هنوز دوباره تشخیص داده نمی شود!)",
+ "diagnosis_failed_for_category": "عیب یابی برای دسته '{category}' ناموفق بود: {error}",
+ "diagnosis_display_tip": "برای مشاهده مسائل پیدا شده ، می توانید به بخش تشخیص webadmin بروید یا از خط فرمان 'yunohost diagnosis show --issues --human-readable' را اجرا کنید.",
+ "diagnosis_package_installed_from_sury_details": "برخی از بسته ها ناخواسته از مخزن شخص ثالث به نام Sury نصب شده اند. تیم YunoHost استراتژی مدیریت این بسته ها را بهبود بخشیده ، اما انتظار می رود برخی از تنظیماتی که برنامه های PHP7.3 را در حالی که هنوز بر روی Stretch نصب شده اند نصب کرده اند ، ناسازگاری های باقی مانده ای داشته باشند. برای رفع این وضعیت ، باید دستور زیر را اجرا کنید: {cmd_to_fix} ",
+ "diagnosis_package_installed_from_sury": "برخی از بسته های سیستمی باید کاهش یابد",
+ "diagnosis_backports_in_sources_list": "به نظر می رسد apt (مدیریت بسته) برای استفاده از مخزن پشتیبان پیکربندی شده است. مگر اینکه واقعاً بدانید چه کار می کنید ، ما به شدت از نصب بسته های پشتیبان خودداری می کنیم، زیرا به احتمال زیاد باعث ایجاد ناپایداری یا تداخل در سیستم شما می شود.",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "شما نسخه های ناسازگار از بسته های YunoHost را اجرا می کنید... به احتمال زیاد به دلیل ارتقاء ناموفق یا جزئی است.",
+ "diagnosis_basesystem_ynh_main_version": "سرور نسخه YunoHost {main_version} ({repo}) را اجرا می کند",
+ "diagnosis_basesystem_ynh_single_version": "{package} نسخه: {version} ({repo})",
+ "diagnosis_basesystem_kernel": "سرور نسخه {kernel_version} هسته لینوکس را اجرا می کند",
+ "diagnosis_basesystem_host": "سرور نسخه {debian_version} دبیان را اجرا می کند",
+ "diagnosis_basesystem_hardware_model": "مدل سرور {model} میباشد",
+ "diagnosis_basesystem_hardware": "معماری سخت افزاری سرور {virt} {arch} است",
+ "custom_app_url_required": "برای ارتقاء سفارشی برنامه {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 قابل دسترسی نیست. لطفاً برای اطلاعات بیشتر ، دسته \"وب\" را در عیب یابی بررسی کنید. (اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این چک ها استفاده کنید.)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "سوابق DNS برای دامنه '{domain}' با IP این سرور متفاوت است. لطفاً برای اطلاعات بیشتر ، دسته 'DNS records' (پایه) را در عیب یابی بررسی کنید. اگر اخیراً رکورد A خود را تغییر داده اید ، لطفاً منتظر انتشار آن باشید (برخی از چکرهای انتشار DNS بصورت آنلاین در دسترس هستند). (اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این چک ها استفاده کنید.)",
+ "certmanager_domain_cert_not_selfsigned": "گواهی دامنه {domain} خود امضا نشده است. آیا مطمئن هستید که می خواهید آن را جایگزین کنید؟ (برای این کار از '--force' استفاده کنید.)",
+ "certmanager_domain_not_diagnosed_yet": "هنوز هیچ نتیجه تشخیصی و عیب یابی دامنه {domain} وجود ندارد. لطفاً در بخش عیب یابی ، دسته های 'DNS records' و 'Web'مجدداً عیب یابی را اجرا کنید تا بررسی شود که آیا دامنه ای برای گواهی اجازه رمزنگاری آماده است. (یا اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این بررسی ها استفاده کنید.)",
+ "certmanager_certificate_fetching_or_enabling_failed": "تلاش برای استفاده از گواهینامه جدید برای {domain} جواب نداد...",
+ "certmanager_cert_signing_failed": "گواهی جدید امضا نشده است",
+ "certmanager_cert_renew_success": "گواهی اجازه رمزنگاری برای دامنه '{domain}' تمدید شد",
+ "certmanager_cert_install_success_selfsigned": "گواهی خود امضا شده اکنون برای دامنه '{domain}' نصب شده است",
+ "certmanager_cert_install_success": "هم اینک گواهی اجازه رمزگذاری برای دامنه '{domain}' نصب شده است",
+ "certmanager_cannot_read_cert": "هنگام باز کردن گواهینامه فعلی مشکلی پیش آمده است برای دامنه {domain} (فایل: {file}) ، علّت: {reason}",
+ "certmanager_attempt_to_replace_valid_cert": "شما در حال تلاش برای بازنویسی یک گواهی خوب و معتبر برای دامنه {domain} هستید! (استفاده از --force برای bypass)",
+ "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": "نمی توان از روش سریع برای سازماندهی فایل ها در بایگانی استفاده کرد",
+ "backup_system_part_failed": "از بخش سیستم '{part}' پشتیبان گیری نشد",
+ "backup_running_hooks": "درحال اجرای قلاب پشتیبان گیری...",
+ "backup_permission": "مجوز پشتیبان گیری برای {app}",
+ "backup_output_symlink_dir_broken": "فهرست بایگانی شما '{path}' یک پیوند symlink خراب است. شاید فراموش کرده اید که مجدداً محل ذخیره سازی که به آن اشاره می کند را دوباره نصب یا وصل کنید.",
+ "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_no_uncompress_archive_dir": "چنین فهرست بایگانی فشرده نشده ایی وجود ندارد",
+ "backup_mount_archive_for_restore": "در حال آماده سازی بایگانی برای بازگردانی...",
+ "backup_method_tar_finished": "بایگانی پشتیبان TAR ایجاد شد",
+ "backup_method_custom_finished": "روش پشتیبان گیری سفارشی '{method}' به پایان رسید",
+ "backup_method_copy_finished": "نسخه پشتیبان نهایی شد",
+ "backup_hook_unknown": "قلاب پشتیبان '{hook}' ناشناخته است",
+ "backup_deleted": "نسخه پشتیبان حذف شد",
+ "backup_delete_error": "'{path}' حذف نشد",
+ "backup_custom_mount_error": "روش پشتیبان گیری سفارشی نمی تواند از مرحله 'mount' عبور کند",
+ "backup_custom_backup_error": "روش پشتیبان گیری سفارشی نمی تواند مرحله 'backup' را پشت سر بگذارد",
+ "backup_csv_creation_failed": "فایل CSV مورد نیاز برای بازیابی ایجاد نشد",
+ "backup_csv_addition_failed": "فایلهای پشتیبان به فایل CSV اضافه نشد",
+ "backup_creation_failed": "نسخه پشتیبان بایگانی ایجاد نشد",
+ "backup_create_size_estimation": "بایگانی حاوی حدود {size} داده است.",
+ "backup_created": "نسخه پشتیبان ایجاد شد",
+ "backup_couldnt_bind": "نمی توان {src} را به {dest} متصل کرد.",
+ "backup_copying_to_organize_the_archive": "در حال کپی {size} مگابایت برای سازماندهی بایگانی",
+ "backup_cleaning_failed": "پوشه موقت پشتیبان گیری پاکسازی نشد",
+ "backup_cant_mount_uncompress_archive": "بایگانی فشرده سازی نشده را نمی توان به عنوان حفاظت از نوشتن مستقر کرد",
+ "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_app_not_found": "در بایگانی پشتیبان {app} پیدا نشد",
+ "backup_applying_method_tar": "ایجاد آرشیو پشتیبان TAR...",
+ "backup_applying_method_custom": "فراخوانی روش پشتیبان گیری سفارشی '{method}'...",
+ "backup_applying_method_copy": "در حال کپی تمام فایل ها برای پشتیبان گیری...",
+ "backup_app_failed": "{app} پشتیبان گیری نشد",
+ "backup_actually_backuping": "ایجاد آرشیو پشتیبان از پرونده های جمع آوری شده...",
+ "backup_abstract_method": "این روش پشتیبان گیری هنوز اجرا نشده است",
+ "ask_password": "رمز عبور",
+ "ask_new_path": "مسیر جدید",
+ "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": "خطایی در داخل اسکریپت ارتقاء برنامه رخ داده است",
+ "app_upgrade_failed": "{app} ارتقاء نیافت: {error}",
+ "app_upgrade_app_name": "در حال ارتقاء {app}...",
+ "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_install": "در حال نصب {app}...",
+ "app_sources_fetch_failed": "نمی توان فایل های منبع را واکشی کرد ، آیا URL درست است؟",
+ "app_restore_script_failed": "خطایی در داخل اسکریپت بازیابی برنامه رخ داده است",
+ "app_restore_failed": "{app} بازیابی نشد: {error}",
+ "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_manifest_install_ask_path": "مسیر URL (بعد از دامنه) را انتخاب کنید که این برنامه باید در آن نصب شود",
+ "app_manifest_install_ask_domain": "دامنه ای را انتخاب کنید که این برنامه باید در آن نصب شود",
+ "app_manifest_invalid": "مشکلی در مانیفست برنامه وجود دارد: {error}",
+ "app_location_unavailable": "این نشانی وب یا در دسترس نیست یا با برنامه (هایی) که قبلاً نصب شده در تعارض است:\n{apps}",
+ "app_label_deprecated": "این دستور منسوخ شده است! لطفاً برای مدیریت برچسب برنامه از فرمان جدید'yunohost به روز رسانی مجوز کاربر' استفاده کنید.",
+ "app_make_default_location_already_used": "نمی توان '{app}' را برنامه پیش فرض در دامنه قرار داد ، '{domain}' قبلاً توسط '{other_app}' استفاده می شود",
+ "app_install_script_failed": "خطایی در درون اسکریپت نصب برنامه رخ داده است",
+ "app_install_failed": "نصب {app} امکان پذیر نیست: {error}",
+ "app_install_files_invalid": "این فایل ها قابل نصب نیستند",
+ "app_id_invalid": "شناسه برنامه نامعتبر است",
+ "app_full_domain_unavailable": "متأسفیم ، این برنامه باید در دامنه خود نصب شود ، اما سایر برنامه ها قبلاً در دامنه '{domain}' نصب شده اند.شما به جای آن می توانید از یک زیر دامنه اختصاص داده شده به این برنامه استفاده کنید.",
+ "app_extraction_failed": "فایل های نصبی استخراج نشد",
+ "app_change_url_success": "{app} URL اکنون {domain} {path} است",
+ "app_change_url_no_script": "برنامه '{app_name}' هنوز از تغییر URL پشتیبانی نمی کند. شاید باید آن را ارتقا دهید.",
+ "app_change_url_identical_domains": "دامنه /url_path قدیمی و جدیدیکسان هستند ('{domain}{path}') ، کاری برای انجام دادن نیست.",
+ "diagnosis_http_connection_error": "خطای اتصال: ارتباط با دامنه درخواست شده امکان پذیر نیست، به احتمال زیاد غیرقابل دسترسی است.",
+ "diagnosis_http_timeout": "زمان تلاش برای تماس با سرور از خارج به پایان رسید. به نظر می رسد غیرقابل دسترسی است.
1. شایع ترین علت برای این مشکل ، پورت 80 است (و 443) به درستی به سرور شما ارسال نمی شوند.
2. همچنین باید مطمئن شوید که سرویس nginx در حال اجرا است
3. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.",
+ "diagnosis_http_ok": "دامنه {domain} از طریق HTTP از خارج از شبکه محلی قابل دسترسی است.",
+ "diagnosis_http_localdomain": "انتظار نمی رود که دامنه {domain} ، با TLD محلی. از خارج از شبکه محلی به آن دسترسی پیدا کند.",
+ "diagnosis_http_could_not_diagnose_details": "خطا: {error}",
+ "diagnosis_http_could_not_diagnose": "نمی توان تشخیص داد که در IPv{ipversion} دامنه ها از خارج قابل دسترسی هستند یا خیر.",
+ "diagnosis_http_hairpinning_issue_details": "این احتمالاً به دلیل جعبه / روتر ISP شما است. در نتیجه ، افراد خارج از شبکه محلی شما می توانند به سرور شما مطابق انتظار دسترسی پیدا کنند ، اما افراد داخل شبکه محلی (احتمالاً مثل شما؟) هنگام استفاده از نام دامنه یا IP جهانی. ممکن است بتوانید وضعیت را بهبود بخشید با نگاهی به https://yunohost.org/dns_local_network",
+ "diagnosis_http_hairpinning_issue": "به نظر می رسد در شبکه محلی شما hairpinning فعال نشده است.",
+ "diagnosis_ports_forwarding_tip": "برای رفع این مشکل، به احتمال زیاد باید انتقال پورت را در روتر اینترنت خود پیکربندی کنید همانطور که شرح داده شده در https://yunohost.org/isp_box_config",
+ "diagnosis_ports_needed_by": "افشای این پورت برای ویژگی های {category} (سرویس {service}) مورد نیاز است",
+ "diagnosis_ports_ok": "پورت {port} از خارج قابل دسترسی است.",
+ "diagnosis_ports_partially_unreachable": "پورت {port} از خارج در {failed}IPv قابل دسترسی نیست.",
+ "diagnosis_ports_unreachable": "پورت {port} از خارج قابل دسترسی نیست.",
+ "diagnosis_ports_could_not_diagnose_details": "خطا: {error}",
+ "diagnosis_ports_could_not_diagnose": "نمی توان تشخیص داد پورت ها از خارج در IPv{ipversion} قابل دسترسی هستند یا خیر.",
+ "diagnosis_description_regenconf": "تنظیمات سیستم",
+ "diagnosis_description_mail": "ایمیل",
+ "diagnosis_description_web": "وب",
+ "diagnosis_description_ports": "ارائه پورت ها",
+ "diagnosis_description_systemresources": "منابع سیستم",
+ "diagnosis_description_services": "بررسی وضعیّت سرویس ها",
+ "diagnosis_description_dnsrecords": "رکورد DNS",
+ "diagnosis_description_ip": "اتصال به اینترنت",
+ "diagnosis_description_basesystem": "سیستم پایه",
+ "diagnosis_security_vulnerable_to_meltdown_details": "برای رفع این مشکل ، باید سیستم خود را ارتقا دهید و مجدداً راه اندازی کنید تا هسته لینوکس جدید بارگیری شود (یا در صورت عدم کارکرد با ارائه دهنده سرور خود تماس بگیرید). برای اطلاعات بیشتر به https://meltdownattack.com/ مراجعه کنید.",
+ "diagnosis_security_vulnerable_to_meltdown": "به نظر می رسد شما در برابر آسیب پذیری امنیتی بحرانی Meltdown آسیب پذیر هستید",
+ "diagnosis_rootfstotalspace_critical": "کل سیستم فایل فقط دارای {space} است که بسیار نگران کننده است! احتمالاً خیلی زود فضای دیسک شما تمام می شود! توصیه می شود حداقل 16 گیگابایت و بیشتر فضا برای سیستم فایل ریشه داشته باشید.",
+ "diagnosis_rootfstotalspace_warning": "سیستم فایل ریشه در مجموع فقط {space} دارد. ممکن است اشکالی نداشته باشد ، اما مراقب باشید زیرا در نهایت ممکن است فضای دیسک شما به سرعت تمام شود... توصیه می شود حداقل 16 گیگابایت و بیشتر فضا برای سیستم فایل ریشه داشته باشید.",
+ "diagnosis_regenconf_manually_modified_details": "اگر بدانید چه کار می کنید ، احتمالاً خوب است! YunoHost به روز رسانی خودکار این فایل را متوقف می کند... اما مراقب باشید که ارتقاء YunoHost می تواند شامل تغییرات مهم توصیه شده باشد. اگر می خواهید ، می توانید تفاوت ها را با yunohost tools regen-conf {category} --dry-run --with-diff و تنظیم مجدد پیکربندی توصیه شده به زور با فرمان yunohost tools regen-conf {category} --force ",
+ "diagnosis_regenconf_manually_modified": "به نظر می رسد فایل پیکربندی {file}
به صورت دستی اصلاح شده است.",
+ "diagnosis_regenconf_allgood": "همه فایلهای پیکربندی مطابق با تنظیمات توصیه شده است!",
+ "diagnosis_mail_queue_too_big": "تعداد زیادی ایمیل معلق در صف پست ({nb_pending} ایمیل)",
+ "diagnosis_mail_queue_unavailable_details": "خطا: {error}",
+ "diagnosis_mail_queue_unavailable": "نمی توان با تعدادی از ایمیل های معلق در صف مشورت کرد",
+ "diagnosis_mail_queue_ok": "{nb_pending} ایمیل های معلق در صف های ایمیل",
+ "diagnosis_mail_blacklist_website": "پس از شناسایی دلیل لیست شدن و رفع آن، با خیال راحت درخواست کنید IP یا دامنه شما حذف شود از {blacklist_website}",
+ "diagnosis_mail_blacklist_reason": "دلیل لیست سیاه: {reason}",
+ "diagnosis_mail_blacklist_listed_by": "IP یا دامنه شما {item}
در لیست سیاه {blacklist_name} قرار دارد",
+ "diagnosis_mail_blacklist_ok": "به نظر می رسد IP ها و دامنه های مورد استفاده این سرور در لیست سیاه قرار ندارند",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS معکوس فعلی: {rdns_domain}
مقدار مورد انتظار: {ehlo_domain}
",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain": "DNS معکوس به درستی در IPv{ipversion} پیکربندی نشده است. ممکن است برخی از ایمیل ها تحویل داده نشوند یا به عنوان هرزنامه پرچم گذاری شوند.",
+ "diagnosis_mail_fcrdns_nok_alternatives_6": "برخی از ارائه دهندگان به شما اجازه نمی دهند DNS معکوس خود را پیکربندی کنید (یا ممکن است ویژگی آنها شکسته شود...). اگر DNS معکوس شما به درستی برای IPv4 پیکربندی شده است، با استفاده از آن می توانید هنگام ارسال ایمیل، استفاده از IPv6 را غیرفعال کنید. yunohost settings set smtp.allow_ipv6 -v off . توجه: این راه حل آخری به این معنی است که شما نمی توانید از چند سرور IPv6 موجود ایمیل ارسال یا دریافت کنید.",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "برخی از ارائه دهندگان به شما اجازه نمی دهند DNS معکوس خود را پیکربندی کنید (یا ممکن است ویژگی آنها شکسته شود...). اگر به همین دلیل مشکلاتی را تجربه می کنید ، راه حل های زیر را در نظر بگیرید: - برخی از ISP ها جایگزین ارائه می دهند با استفاده از رله سرور ایمیل اگرچه به این معنی است که رله می تواند از ترافیک ایمیل شما جاسوسی کند.
- یک جایگزین دوستدار حریم خصوصی استفاده از VPN * با IP عمومی اختصاصی * برای دور زدن این نوع محدودیت ها است. ببینید https://yunohost.org/#/vpn_advantage
- یا ممکن است به ارائه دهنده دیگری بروید",
+ "diagnosis_mail_fcrdns_nok_details": "ابتدا باید DNS معکوس را پیکربندی کنید با {ehlo_domain}
در رابط روتر اینترنت یا رابط ارائه دهنده میزبانی تان. (ممکن است برخی از ارائه دهندگان میزبانی از شما بخواهند که برای این کار تیکت پشتیبانی ارسال کنید).",
+ "diagnosis_mail_fcrdns_dns_missing": "در IPv{ipversion} هیچ DNS معکوسی تعریف نشده است. ممکن است برخی از ایمیل ها تحویل داده نشوند یا به عنوان هرزنامه پرچم گذاری شوند.",
+ "diagnosis_mail_fcrdns_ok": "DNS معکوس شما به درستی پیکربندی شده است!",
+ "diagnosis_mail_ehlo_could_not_diagnose_details": "خطا: {error}",
+ "diagnosis_mail_ehlo_could_not_diagnose": "نمی توان تشخیص داد که آیا سرور ایمیل postfix از خارج در IPv{ipversion} قابل دسترسی است یا خیر.",
+ "diagnosis_mail_ehlo_wrong_details": "EHLO دریافت شده توسط تشخیص دهنده از راه دور در IPv{ipversion} با دامنه سرور شما متفاوت است.
EHLO دریافت شده: {wrong_ehlo}
انتظار می رود: {right_ehlo}
شایع ترین علت این مشکل ، پورت 25 است به درستی به سرور شما ارسال نشده است. از سوی دیگر اطمینان حاصل کنید که هیچ فایروال یا پروکسی معکوسی تداخل ایجاد نمی کند.",
+ "diagnosis_mail_ehlo_wrong": "یک سرور ایمیل SMTP متفاوت در IPv{ipversion} پاسخ می دهد. سرور شما احتمالاً نمی تواند ایمیل دریافت کند.",
+ "diagnosis_mail_ehlo_bad_answer_details": "ممکن است به دلیل پاسخ دادن دستگاه دیگری به جای سرور شما باشد.",
+ "diagnosis_mail_ehlo_bad_answer": "یک سرویس غیر SMTP در پورت 25 در IPv{ipversion} پاسخ داد",
+ "diagnosis_mail_ehlo_unreachable_details": "اتصال روی پورت 25 سرور شما در IPv{ipversion} باز نشد. به نظر می رسد غیرقابل دسترس است.
1. شایع ترین علت این مشکل ، پورت 25 است به درستی به سرور شما ارسال نشده است.
2. همچنین باید مطمئن شوید که سرویس postfix در حال اجرا است.
3. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.",
+ "diagnosis_mail_ehlo_unreachable": "سرور ایمیل SMTP از خارج در IPv {ipversion} غیرقابل دسترسی است. قادر به دریافت ایمیل نخواهد بود.",
+ "diagnosis_mail_ehlo_ok": "سرور ایمیل SMTP از خارج قابل دسترسی است و بنابراین می تواند ایمیل دریافت کند!",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "برخی از ارائه دهندگان به شما اجازه نمی دهند پورت خروجی 25 را رفع انسداد کنید زیرا به بی طرفی شبکه اهمیتی نمی دهند.
- برخی از آنها جایگزین را ارائه می دهند با استفاده از رله سرور ایمیل اگرچه به این معنی است که رله می تواند از ترافیک ایمیل شما جاسوسی کند.
- یک جایگزین دوستدار حریم خصوصی استفاده از VPN * با IP عمومی اختصاصی * برای دور زدن این نوع محدودیت ها است. ببینید https://yunohost.org/#/vpn_advantage
- همچنین می توانید تغییر را در نظر بگیرید به یک ارائه دهنده بی طرف خالص تر",
+ "diagnosis_mail_outgoing_port_25_blocked_details": "ابتدا باید سعی کنید پورت خروجی 25 را در رابط اینترنت روتر یا رابط ارائه دهنده میزبانی خود باز کنید. (ممکن است برخی از ارائه دهندگان میزبانی از شما بخواهند که برای این کار تیکت پشتیبانی ارسال کنید).",
+ "diagnosis_mail_outgoing_port_25_blocked": "سرور ایمیل SMTP نمی تواند به سرورهای دیگر ایمیل ارسال کند زیرا درگاه خروجی 25 در IPv {ipversion} مسدود شده است.",
+ "diagnosis_mail_outgoing_port_25_ok": "سرور ایمیل SMTP قادر به ارسال ایمیل است (پورت خروجی 25 مسدود نشده است).",
+ "diagnosis_swap_tip": "لطفاً مراقب و آگاه باشید، اگر سرور میزبانی swap را روی کارت SD یا حافظه SSD انجام دهد ، ممکن است طول عمر دستگاه را به شدت کاهش دهد.",
+ "diagnosis_swap_ok": "سیستم {total} swap دارد!",
+ "diagnosis_swap_notsomuch": "سیستم فقط {total} swap دارد. برای جلوگیری از شرایطی که حافظه سیستم شما تمام می شود ، باید حداقل {recommended} را در نظر بگیرید.",
+ "diagnosis_swap_none": "این سیستم به هیچ وجه swap ندارد. برای جلوگیری از شرایطی که حافظه سیستم شما تمام می شود ، باید حداقل {recommended} swap را در نظر بگیرید.",
+ "diagnosis_ram_ok": "این سیستم هنوز {available} ({available_percent}٪) حافظه در دسترس دارد از مجموع {total}.",
+ "diagnosis_ram_low": "این سیستم فقط {available} ({available_percent}٪) حافظه در دسترس دارد! (از {total}). مراقب باشید.",
+ "diagnosis_ram_verylow": "این سیستم فقط {available} ({available_percent}٪) حافظه در دسترس دارد! (از {total})",
+ "diagnosis_diskusage_ok": "ذخیره سازی {mountpoint}
(روی دستگاه {device}
) هنوز {free} فضا در دسترس دارد ({free_percent}%) فضای باقی مانده (از {total})!",
+ "diagnosis_http_nginx_conf_not_up_to_date": "به نظر می رسد که پیکربندی nginx این دامنه به صورت دستی تغییر کرده است و از تشخیص YunoHost در صورت دسترسی به HTTP جلوگیری می کند.",
+ "diagnosis_http_partially_unreachable": "به نظر می رسد که دامنه {domain} از طریق HTTP از خارج از شبکه محلی در IPv{failed} غیرقابل دسترسی است، اگرچه در IPv{passed} کار می کند.",
+ "diagnosis_http_unreachable": "به نظر می رسد دامنه {domain} از خارج از شبکه محلی از طریق HTTP قابل دسترسی نیست.",
+ "diagnosis_http_bad_status_code": "به نظر می رسد دستگاه دیگری (شاید روتر اینترنتی شما) به جای سرور شما پاسخ داده است.
1. شایع ترین علت برای این مشکل ، پورت 80 است (و 443) به درستی به سرور شما ارسال نمی شوند.
2. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.",
+ "disk_space_not_sufficient_update": "برای به روزرسانی این برنامه فضای دیسک کافی باقی نمانده است",
+ "disk_space_not_sufficient_install": "فضای کافی برای نصب این برنامه در دیسک باقی نمانده است",
+ "diagnosis_sshd_config_inconsistent_details": "لطفاً اجراکنید yunohost settings set security.ssh.port -v YOUR_SSH_PORT برای تعریف پورت SSH و بررسی کنید yunohost tools regen-conf ssh --dry-run --with-diff و yunohost tools regen-conf ssh --force برای تنظیم مجدد تنظیمات خود به توصیه YunoHost.",
+ "diagnosis_sshd_config_inconsistent": "به نظر می رسد که پورت SSH به صورت دستی در/etc/ssh/sshd_config تغییر یافته است. از زمان YunoHost 4.2 ، یک تنظیم جهانی جدید 'security.ssh.port' برای جلوگیری از ویرایش دستی پیکربندی در دسترس است.",
+ "diagnosis_sshd_config_insecure": "به نظر می رسد که پیکربندی SSH به صورت دستی تغییر یافته است و مطمئن نیست زیرا هیچ دستورالعمل 'AllowGroups' یا 'AllowUsers' برای محدود کردن دسترسی به کاربران مجاز ندارد.",
+ "diagnosis_processes_killed_by_oom_reaper": "برخی از فرآیندها اخیراً توسط سیستم از بین رفته اند زیرا حافظه آن تمام شده است. این به طور معمول نشانه کمبود حافظه در سیستم یا فرآیندی است که حافظه زیادی را از بین می برد. خلاصه فرآیندهای کشته شده:\n{kills_summary}",
+ "diagnosis_never_ran_yet": "به نظر می رسد این سرور به تازگی راه اندازی شده است و هنوز هیچ گزارش تشخیصی برای نمایش وجود ندارد. شما باید با اجرای یک عیب یابی و تشخیص کامل، از طریق رابط مدیریت تحت وب webadmin یا با استفاده از 'yunohost diagnosis run' از خط فرمان معاینه و تشخیص عیب یابی را شروع کنید.",
+ "diagnosis_unknown_categories": "دسته های زیر ناشناخته است: {categories}",
+ "diagnosis_http_nginx_conf_not_up_to_date_details": "برای برطرف کردن وضعیّت ، تفاوت را با استفاده از خط فرمان بررسی کنیدyunohost tools regen-conf nginx --dry-run --with-diff و اگر خوب است ، تغییرات را اعمال کنید با استفاده از فرمان yunohost tools regen-conf nginx --force .",
+ "domain_cannot_remove_main_add_new_one": "شما نمی توانید '{domain}' را حذف کنید زیرا دامنه اصلی و تنها دامنه شما است، ابتدا باید دامنه دیگری را با 'yunohost domain add ' اضافه کنید، سپس با استفاده از 'yunohost domain main-domain -n ' به عنوان دامنه اصلی تنظیم شده. و بعد از آن می توانید'{domain}' را حذف کنید با استفاده از'yunohost domain remove {domain}'.'",
+ "domain_cannot_add_xmpp_upload": "شما نمی توانید دامنه هایی را که با \"xmpp-upload\" شروع می شوند اضافه کنید. این نوع نام مختص ویژگی بارگذاری XMPP است که در YunoHost یکپارچه شده است.",
+ "domain_cannot_remove_main": "شما نمی توانید '{domain}' را حذف کنید زیرا دامنه اصلی است ، ابتدا باید با استفاده از 'yunohost domain main-domain -n ' دامنه دیگری را به عنوان دامنه اصلی تعیین کنید. در اینجا لیست دامنه های کاندید وجود دارد: {other_domains}",
+ "installation_complete": "عملیّات نصب کامل شد",
+ "hook_name_unknown": "نام قلاب ناشناخته '{name}'",
+ "hook_list_by_invalid": "از این ویژگی نمی توان برای فهرست قلاب ها استفاده کرد",
+ "hook_json_return_error": "بازگشت از قلاب {path} خوانده نشد. خطا: {msg}. محتوای خام: {raw_content}",
+ "hook_exec_not_terminated": "اسکریپت به درستی به پایان نرسید: {path}",
+ "hook_exec_failed": "اسکریپت اجرا نشد: {path}",
+ "group_user_not_in_group": "کاربر {user} در گروه {group} نیست",
+ "group_user_already_in_group": "کاربر {user} در حال حاضر در گروه {group} است",
+ "group_update_failed": "گروه '{group}' به روز نشد: {error}",
+ "group_updated": "گروه '{group}' به روز شد",
+ "group_unknown": "گروه '{group}' ناشناخته است",
+ "group_deletion_failed": "گروه '{group}' حذف نشد: {error}",
+ "group_deleted": "گروه '{group}' حذف شد",
+ "group_cannot_be_deleted": "گروه {group} را نمی توان به صورت دستی حذف کرد.",
+ "group_cannot_edit_primary_group": "گروه '{group}' را نمی توان به صورت دستی ویرایش کرد. این گروه اصلی شامل تنها یک کاربر خاص است.",
+ "group_cannot_edit_visitors": "ویرایش گروه 'visitors' بازدیدکنندگان به صورت دستی امکان پذیر نیست. این گروه ویژه، نمایانگر بازدیدکنندگان ناشناس است",
+ "group_cannot_edit_all_users": "گروه 'all_users' را نمی توان به صورت دستی ویرایش کرد. این یک گروه ویژه است که شامل همه کاربران ثبت شده در YunoHost میباشد",
+ "group_creation_failed": "گروه '{group}' ایجاد نشد: {error}",
+ "group_created": "گروه '{group}' ایجاد شد",
+ "group_already_exist_on_system_but_removing_it": "گروه {group} از قبل در گروه های سیستم وجود دارد ، اما YunoHost آن را حذف می کند...",
+ "group_already_exist_on_system": "گروه {group} از قبل در گروه های سیستم وجود دارد",
+ "group_already_exist": "گروه {group} از قبل وجود دارد",
+ "good_practices_about_user_password": "گذرواژه باید حداقل 8 کاراکتر باشد - اگرچه استفاده از گذرواژه طولانی تر تمرین خوبی است (به عنوان مثال عبارت عبور) و/یا استفاده از تنوع کاراکترها (بزرگ ، کوچک ، رقم و کاراکتر های خاص).",
+ "good_practices_about_admin_password": "اکنون می خواهید گذرواژه جدیدی برای مدیریت تعریف کنید. گذرواژه باید حداقل 8 کاراکتر باشد - اگرچه استفاده از گذرواژه طولانی تر تمرین خوبی است (به عنوان مثال عبارت عبور) و/یا استفاده از تنوع کاراکترها (بزرگ ، کوچک ، رقم و کاراکتر های خاص).",
+ "global_settings_unknown_type": "وضعیت غیرمنتظره ، به نظر می رسد که تنظیمات {setting} دارای نوع {unknown_type} است اما از نوع پشتیبانی شده توسط سیستم نیست.",
+ "global_settings_setting_backup_compress_tar_archives": "هنگام ایجاد پشتیبان جدید ، بایگانی های فشرده (.tar.gz) را به جای بایگانی های فشرده نشده (.tar) انتخاب کنید. N.B. : فعال کردن این گزینه به معنای ایجاد آرشیوهای پشتیبان سبک تر است ، اما روش پشتیبان گیری اولیه به طور قابل توجهی طولانی تر و سنگین تر بر روی CPU خواهد بود.",
+ "global_settings_setting_security_experimental_enabled": "فعال کردن ویژگی های امنیتی آزمایشی (اگر نمی دانید در حال انجام چه کاری هستید این کار را انجام ندهید!)",
+ "global_settings_setting_security_webadmin_allowlist": "آدرس های IP که مجاز به دسترسی مدیر وب هستند. جدا شده با ویرگول.",
+ "global_settings_setting_security_webadmin_allowlist_enabled": "فقط به برخی از IP ها اجازه دسترسی به مدیریت وب را بدهید.",
+ "global_settings_setting_smtp_relay_password": "رمز عبور میزبان رله SMTP",
+ "global_settings_setting_smtp_relay_user": "حساب کاربری رله SMTP",
+ "global_settings_setting_smtp_relay_port": "پورت رله SMTP",
+ "global_settings_setting_smtp_relay_host": "میزبان رله SMTP برای ارسال نامه به جای این نمونه yunohost استفاده می شود. اگر در یکی از این شرایط قرار دارید مفید است: پورت 25 شما توسط ارائه دهنده ISP یا VPS شما مسدود شده است، شما یک IP مسکونی دارید که در DUHL ذکر شده است، نمی توانید DNS معکوس را پیکربندی کنید یا این سرور مستقیماً در اینترنت نمایش داده نمی شود و می خواهید از یکی دیگر برای ارسال ایمیل استفاده کنید.",
+ "global_settings_setting_smtp_allow_ipv6": "اجازه دهید از IPv6 برای دریافت و ارسال نامه استفاده شود",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "همپوشانی پانل SSOwat را فعال کنید",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "اجازه دهید از کلید میزبان DSA (منسوخ شده) برای پیکربندی SH daemon استفاده شود",
+ "global_settings_unknown_setting_from_settings_file": "کلید ناشناخته در تنظیمات: '{setting_key}'، آن را کنار گذاشته و در /etc/yunohost/settings-unknown.json ذخیره کنید",
+ "global_settings_setting_security_ssh_port": "درگاه SSH",
+ "global_settings_setting_security_postfix_compatibility": "سازگاری در مقابل مبادله امنیتی برای سرور Postfix. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد",
+ "global_settings_setting_security_ssh_compatibility": "سازگاری در مقابل مبادله امنیتی برای سرور SSH. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد",
+ "global_settings_setting_security_password_user_strength": "قدرت رمز عبور کاربر",
+ "global_settings_setting_security_password_admin_strength": "قدرت رمز عبور مدیر",
+ "global_settings_setting_security_nginx_compatibility": "سازگاری در مقابل مبادله امنیتی برای وب سرور NGINX. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد",
+ "global_settings_setting_pop3_enabled": "پروتکل POP3 را برای سرور ایمیل فعال کنید",
+ "global_settings_reset_success": "تنظیمات قبلی اکنون در {path} پشتیبان گیری شده است",
+ "global_settings_key_doesnt_exists": "کلید '{settings_key}' در تنظیمات جهانی وجود ندارد ، با اجرای 'لیست تنظیمات yunohost' می توانید همه کلیدهای موجود را مشاهده کنید",
+ "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": "فایل {path} وجود ندارد.",
+ "field_invalid": "فیلد نامعتبر '{}'",
+ "experimental_feature": "هشدار: این ویژگی آزمایشی است و پایدار تلقی نمی شود ، نباید از آن استفاده کنید مگر اینکه بدانید در حال انجام چه کاری هستید.",
+ "extracting": "استخراج...",
+ "dyndns_unavailable": "دامنه '{domain}' در دسترس نیست.",
+ "dyndns_domain_not_provided": "ارائه دهنده DynDNS {provider} نمی تواند دامنه {domain} را ارائه دهد.",
+ "dyndns_registration_failed": "دامنه DynDNS ثبت نشد: {error}",
+ "dyndns_registered": "دامنه DynDNS ثبت شد",
+ "dyndns_provider_unreachable": "دسترسی به ارائه دهنده DynDNS {provider} امکان پذیر نیست: یا YunoHost شما به درستی به اینترنت متصل نیست یا سرور dynette خراب است.",
+ "dyndns_no_domain_registered": "هیچ دامنه ای با DynDNS ثبت نشده است",
+ "dyndns_key_not_found": "کلید DNS برای دامنه یافت نشد",
+ "dyndns_key_generating": "ایجاد کلید DNS... ممکن است مدتی طول بکشد.",
+ "dyndns_ip_updated": "IP خود را در DynDNS به روز کرد",
+ "dyndns_ip_update_failed": "آدرس IP را به DynDNS به روز نکرد",
+ "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` and/or `sudo dpkg --configure -a` مشکل را حل کنید.",
+ "downloading": "در حال بارگیری...",
+ "done": "انجام شد",
+ "domains_available": "دامنه های موجود:",
+ "domain_name_unknown": "دامنه '{domain}' ناشناخته است",
+ "domain_uninstall_app_first": "این برنامه ها هنوز روی دامنه شما نصب هستند:\n{apps}\n\nلطفاً قبل از اقدام به حذف دامنه ، آنها را با استفاده از 'برنامه yunohost remove the_app_id' حذف کرده یا با استفاده از 'yunohost app change-url the_app_id' به دامنه دیگری منتقل کنید",
+ "domain_remove_confirm_apps_removal": "حذف این دامنه برنامه های زیر را حذف می کند:\n{apps}\n\nآیا طمئن هستید که میخواهید انجام دهید؟ [{answers}]",
+ "domain_hostname_failed": "نام میزبان جدید قابل تنظیم نیست. این ممکن است بعداً مشکلی ایجاد کند (ممکن هم هست خوب باشد).",
+ "domain_exists": "دامنه از قبل وجود دارد",
+ "domain_dyndns_root_unknown": "دامنه ریشه DynDNS ناشناخته",
+ "domain_dyndns_already_subscribed": "شما قبلاً در یک دامنه DynDNS مشترک شده اید",
+ "domain_dns_conf_is_just_a_recommendation": "این دستور پیکربندی * توصیه شده * را به شما نشان می دهد. این وظیفه شماست که مطابق این توصیه ، منطقه DNS خود را در ثبت کننده خود پیکربندی کنید. در واقع پیکربندی DNS را برای شما تنظیم نمی کند.",
+ "domain_deletion_failed": "حذف دامنه {domain} امکان پذیر نیست: {error}",
+ "domain_deleted": "دامنه حذف شد",
+ "domain_creation_failed": "ایجاد دامنه {domain} امکان پذیر نیست: {error}",
+ "domain_created": "دامنه ایجاد شد",
+ "domain_cert_gen_failed": "گواهی تولید نشد",
+ "permission_creation_failed": "مجوز '{permission}' را نمیتوان ایجاد کرد: {error}",
+ "permission_created": "مجوز '{permission}' ایجاد شد",
+ "permission_cannot_remove_main": "حذف مجوز اصلی مجاز نیست",
+ "permission_already_up_to_date": "مجوز به روز نشد زیرا درخواست های افزودن/حذف هم اینک با وضعیّت فعلی مطابقت دارد.",
+ "permission_already_exist": "مجوز '{permission}' در حال حاضر وجود دارد",
+ "permission_already_disallowed": "گروه '{group}' قبلاً مجوز '{permission}' را غیرفعال کرده است",
+ "permission_already_allowed": "گروه '{group}' قبلاً مجوز '{permission}' را فعال کرده است",
+ "pattern_password_app": "متأسفیم ، گذرواژه ها نمی توانند شامل کاراکترهای زیر باشند: {forbidden_chars}",
+ "pattern_username": "باید فقط حروف الفبایی کوچک و خط زیر باشد",
+ "pattern_port_or_range": "باید یک شماره پورت معتبر (یعنی 0-65535) یا محدوده پورت (به عنوان مثال 100: 200) باشد",
+ "pattern_password": "باید حداقل 3 کاراکتر داشته باشد",
+ "pattern_mailbox_quota": "باید اندازه ای با پسوند b / k / M / G / T یا 0 داشته باشد تا سهمیه نداشته باشد",
+ "pattern_lastname": "باید نام خانوادگی معتبر باشد",
+ "pattern_firstname": "باید یک نام کوچک معتبر باشد",
+ "pattern_email": "باید یک آدرس ایمیل معتبر باشد ، بدون نماد '+' (به عنوان مثال someone@example.com)",
+ "pattern_email_forward": "باید یک آدرس ایمیل معتبر باشد ، نماد '+' پذیرفته شده است (به عنوان مثال someone+tag@example.com)",
+ "pattern_domain": "باید یک نام دامنه معتبر باشد (به عنوان مثال my-domain.org)",
+ "pattern_backup_archive_name": "باید یک نام فایل معتبر با حداکثر 30 کاراکتر حرف و عدد و -_ باشد. فقط کاراکترها",
+ "password_too_simple_4": "گذرواژه باید حداقل 12 کاراکتر طول داشته باشد و شامل عدد ، حروف الفبائی کوچک و بزرگ و کاراکترهای خاص باشد",
+ "password_too_simple_3": "گذرواژه باید حداقل 8 کاراکتر طول داشته باشد و شامل عدد ، حروف الفبائی کوچک و بزرگ و کاراکترهای خاص باشد",
+ "password_too_simple_2": "گذرواژه باید حداقل 8 کاراکتر طول داشته باشد و شامل عدد ، حروف الفبائی کوچک و بزرگ باشد",
+ "password_too_simple_1": "رمز عبور باید حداقل 8 کاراکتر باشد",
+ "password_listed": "این رمز در بین پر استفاده ترین رمزهای عبور در جهان قرار دارد. لطفاً چیزی منحصر به فرد تر انتخاب کنید.",
+ "packages_upgrade_failed": "همه بسته ها را نمی توان ارتقا داد",
+ "operation_interrupted": "عملیات به صورت دستی قطع شد؟",
+ "invalid_password": "رمز عبور نامعتبر",
+ "invalid_number": "باید یک عدد باشد",
+ "not_enough_disk_space": "فضای آزاد کافی در '{path}' وجود ندارد",
+ "migrations_to_be_ran_manually": "مهاجرت {id} باید به صورت دستی اجرا شود. لطفاً به صفحه Tools → Migrations در صفحه webadmin بروید، یا `yunohost tools migrations run` را اجرا کنید.",
+ "migrations_success_forward": "مهاجرت {id} تکمیل شد",
+ "migrations_skip_migration": "رد کردن مهاجرت {id}...",
+ "migrations_running_forward": "مهاجرت در حال اجرا {id}»...",
+ "migrations_pending_cant_rerun": "این مهاجرت ها هنوز در انتظار هستند ، بنابراین نمی توان آنها را دوباره اجرا کرد: {ids}",
+ "migrations_not_pending_cant_skip": "این مهاجرت ها معلق نیستند ، بنابراین نمی توان آنها را رد کرد: {ids}",
+ "migrations_no_such_migration": "مهاجرتی به نام '{id}' وجود ندارد",
+ "migrations_no_migrations_to_run": "مهاجرتی برای اجرا وجود ندارد",
+ "migrations_need_to_accept_disclaimer": "برای اجرای مهاجرت {id} ، باید سلب مسئولیت زیر را بپذیرید:\n---\n{disclaimer}\n---\nاگر می خواهید مهاجرت را اجرا کنید ، لطفاً فرمان را با گزینه '--accept-disclaimer' دوباره اجرا کنید.",
+ "migrations_must_provide_explicit_targets": "هنگام استفاده '--skip' یا '--force-rerun' باید اهداف مشخصی را ارائه دهید",
+ "migrations_migration_has_failed": "مهاجرت {id} کامل نشد ، لغو شد. خطا: {exception}",
+ "migrations_loading_migration": "بارگیری مهاجرت {id}...",
+ "migrations_list_conflict_pending_done": "شما نمیتوانید از هر دو انتخاب '--previous' و '--done' به طور همزمان استفاده کنید.",
+ "migrations_exclusive_options": "'--auto', '--skip'، و '--force-rerun' گزینه های متقابل هستند.",
+ "migrations_failed_to_load_migration": "مهاجرت بار نشد {id}: {error}",
+ "migrations_dependencies_not_satisfied": "این مهاجرت ها را اجرا کنید: '{dependencies_id}' ، قبل از مهاجرت {id}.",
+ "migrations_cant_reach_migration_file": "دسترسی به پرونده های مهاجرت در مسیر '٪ s' امکان پذیر نیست",
+ "migrations_already_ran": "این مهاجرت ها قبلاً انجام شده است: {ids}",
+ "migration_0019_slapd_config_will_be_overwritten": "به نظر می رسد که شما پیکربندی slapd را به صورت دستی ویرایش کرده اید. برای این مهاجرت بحرانی ، YunoHost باید به روز رسانی پیکربندی slapd را مجبور کند. فایلهای اصلی در {conf_backup_folder} پشتیبان گیری می شوند.",
+ "migration_0019_add_new_attributes_in_ldap": "اضافه کردن ویژگی های جدید برای مجوزها در پایگاه داده LDAP",
+ "migration_0018_failed_to_reset_legacy_rules": "تنظیم مجدد قوانین iptables قدیمی انجام نشد: {error}",
+ "migration_0018_failed_to_migrate_iptables_rules": "انتقال قوانین قدیمی iptables به nftables انجام نشد: {error}",
+ "migration_0017_not_enough_space": "فضای کافی در {path} برای اجرای مهاجرت در دسترس قرار دهید.",
+ "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 نصب شده است ، اما postgresql 11 نه؟ ممکن است اتفاق عجیبی در سیستم شما رخ داده باشد:(...",
+ "migration_0017_postgresql_96_not_installed": "PostgreSQL روی سیستم شما نصب نشده است. کاری برای انجام دادن نیست.",
+ "migration_0015_weak_certs": "گواهینامه های زیر هنوز از الگوریتم های امضای ضعیف استفاده می کنند و برای سازگاری با نسخه بعدی nginx باید ارتقاء یابند: {certs}",
+ "migration_0015_cleaning_up": "پاک کردن حافظه پنهان و بسته ها دیگر مفید نیست...",
+ "migration_0015_specific_upgrade": "شروع به روزرسانی بسته های سیستم که باید به طور مستقل ارتقا یابد...",
+ "migration_0015_modified_files": "لطفاً توجه داشته باشید که فایل های زیر به صورت دستی اصلاح شده اند و ممکن است پس از ارتقاء رونویسی شوند: {manually_modified_files}",
+ "migration_0015_problematic_apps_warning": "لطفاً توجه داشته باشید که احتمالاً برنامه های نصب شده مشکل ساز تشخیص داده شده. به نظر می رسد که آنها از فهرست برنامه YunoHost نصب نشده اند یا به عنوان 'working' علامت گذاری نشده اند. در نتیجه ، نمی توان تضمین کرد که پس از ارتقاء همچنان کار خواهند کرد: {problematic_apps}",
+ "migration_0015_general_warning": "لطفاً توجه داشته باشید که این مهاجرت یک عملیات ظریف است. تیم YunoHost تمام تلاش خود را برای بررسی و آزمایش آن انجام داد ، اما مهاجرت ممکن است بخشهایی از سیستم یا برنامه های آن را خراب کند.\n\nبنابراین ، توصیه می شود:\n- پشتیبان گیری از هرگونه داده یا برنامه حیاتی را انجام دهید. اطلاعات بیشتر در https://yunohost.org/backup ؛\n- پس از راه اندازی مهاجرت صبور باشید: بسته به اتصال به اینترنت و سخت افزار شما ، ممکن است چند ساعت طول بکشد تا همه چیز ارتقا یابد.",
+ "migration_0015_system_not_fully_up_to_date": "سیستم شما کاملاً به روز نیست. لطفاً قبل از اجرای مهاجرت به Buster ، یک ارتقاء منظم انجام دهید.",
+ "migration_0015_not_enough_free_space": "فضای آزاد در /var /بسیار کم است! برای اجرای این مهاجرت باید حداقل 1 گیگابایت فضای آزاد داشته باشید.",
+ "migration_0015_not_stretch": "توزیع دبیان فعلی استرچ نیست!",
+ "migration_0015_yunohost_upgrade": "شروع به روز رسانی اصلی YunoHost...",
+ "migration_0015_still_on_stretch_after_main_upgrade": "هنگام ارتقاء اصلی مشکلی پیش آمد ، به نظر می رسد سیستم هنوز در Debian Stretch است",
+ "migration_0015_main_upgrade": "شروع به روزرسانی اصلی...",
+ "migration_0015_patching_sources_list": "وصله منابع. لیست ها...",
+ "migration_0015_start": "شروع مهاجرت به باستر",
+ "migration_update_LDAP_schema": "در حال به روزرسانی طرح وشمای LDAP...",
+ "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_description_0020_ssh_sftp_permissions": "پشتیبانی مجوزهای SSH و SFTP را اضافه کنید",
+ "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": "انتقال فایلهای conf php7.0-fpm 'pool' به php7.3",
+ "migration_description_0015_migrate_to_buster": "سیستم را به Debian Buster و YunoHost 4.x ارتقا دهید",
+ "migrating_legacy_permission_settings": "در حال انتقال تنظیمات مجوز قدیمی...",
+ "main_domain_changed": "دامنه اصلی تغییر کرده است",
+ "main_domain_change_failed": "تغییر دامنه اصلی امکان پذیر نیست",
+ "mail_unavailable": "این آدرس ایمیل محفوظ است و باید به طور خودکار به اولین کاربر اختصاص داده شود",
+ "mailbox_used_space_dovecot_down": "اگر می خواهید فضای صندوق پستی استفاده شده را واکشی کنید ، سرویس صندوق پستی Dovecot باید فعال باشد",
+ "mailbox_disabled": "ایمیل برای کاربر {user} خاموش است",
+ "mail_forward_remove_failed": "ارسال ایمیل '{mail}' حذف نشد",
+ "mail_domain_unknown": "آدرس ایمیل نامعتبر برای دامنه '{domain}'. لطفاً از دامنه ای که توسط این سرور اداره می شود استفاده کنید.",
+ "mail_alias_remove_failed": "نام مستعار ایمیل '{mail}' حذف نشد",
+ "log_tools_reboot": "سرور خود را راه اندازی مجدد کنید",
+ "log_tools_shutdown": "سرور خود را خاموش کنید",
+ "log_tools_upgrade": "بسته های سیستم را ارتقا دهید",
+ "log_tools_postinstall": "اسکریپت پس از نصب سرور YunoHost خود را نصب کنید",
+ "log_tools_migrations_migrate_forward": "اجرای مهاجرت ها",
+ "log_domain_main_domain": "'{}' را دامنه اصلی کنید",
+ "log_user_permission_reset": "بازنشانی مجوز '{}'",
+ "log_user_permission_update": "دسترسی ها را برای مجوزهای '{}' به روز کنید",
+ "log_user_update": "به روزرسانی اطلاعات کاربر '{}'",
+ "log_user_group_update": "به روزرسانی گروه '{}'",
+ "log_user_group_delete": "حذف گروه '{}'",
+ "log_user_group_create": "ایجاد گروه '{}'",
+ "log_user_delete": "کاربر '{}' را حذف کنید",
+ "log_user_create": "کاربر '{}' را اضافه کنید",
+ "log_regen_conf": "بازسازی تنظیمات سیستم '{}'",
+ "log_letsencrypt_cert_renew": "تمدید '{}' گواهی اجازه رمزگذاری",
+ "log_selfsigned_cert_install": "گواهی خود امضا شده را در دامنه '{}' نصب کنید",
+ "log_permission_url": "به روزرسانی نشانی اینترنتی مربوط به مجوز دسترسی '{}'",
+ "log_permission_delete": "حذف مجوز دسترسی '{}'",
+ "log_permission_create": "ایجاد مجوز دسترسی '{}'",
+ "log_letsencrypt_cert_install": "گواهی اجازه رمزگذاری را در دامنه '{}' نصب کنید",
+ "log_dyndns_update": "IP مرتبط با '{}' زیر دامنه YunoHost خود را به روز کنید",
+ "log_dyndns_subscribe": "مشترک شدن در زیر دامنه YunoHost '{}'",
+ "log_domain_remove": "دامنه '{}' را از پیکربندی سیستم حذف کنید",
+ "log_domain_add": "دامنه '{}' را به پیکربندی سیستم اضافه کنید",
+ "log_remove_on_failed_install": "پس از نصب ناموفق '{}' را حذف کنید",
+ "log_remove_on_failed_restore": "پس از بازیابی ناموفق از بایگانی پشتیبان، '{}' را حذف کنید",
+ "log_backup_restore_app": "بازیابی '{}' از بایگانی پشتیبان",
+ "log_backup_restore_system": "بازیابی سیستم بوسیله آرشیو پشتیبان",
+ "log_backup_create": "بایگانی پشتیبان ایجاد کنید",
+ "log_available_on_yunopaste": "این گزارش اکنون از طریق {url} در دسترس است",
+ "log_app_action_run": "عملکرد برنامه '{}' را اجرا کنید",
+ "log_app_makedefault": "\"{}\" را برنامه پیش فرض قرار دهید",
+ "log_app_upgrade": "برنامه '{}' را ارتقاء دهید",
+ "log_app_remove": "برنامه '{}' را حذف کنید",
+ "log_app_install": "برنامه '{}' را نصب کنید",
+ "log_app_change_url": "نشانی وب برنامه '{}' را تغییر دهید",
+ "log_operation_unit_unclosed_properly": "واحد عملیّات به درستی بسته نشده است",
+ "log_does_exists": "هیچ گزارش عملیاتی با نام '{log}' وجود ندارد ، برای مشاهده همه گزارش عملیّات های موجود در خط فرمان از دستور 'yunohost log list' استفاده کنید",
+ "log_help_to_get_failed_log": "عملیات '{desc}' کامل نشد. لطفاً برای دریافت راهنمایی و کمک ، گزارش کامل این عملیات را با استفاده از دستور 'yunohost log share {name}' به اشتراک بگذارید",
+ "log_link_to_failed_log": "عملیّات '{desc}' کامل نشد. لطفاً گزارش کامل این عملیات را ارائه دهید بواسطه اینجا را کلیک کنید برای دریافت کمک",
+ "log_help_to_get_log": "برای مشاهده گزارش عملیات '{desc}'، از دستور 'yunohost log show {name}' استفاده کنید",
+ "log_link_to_log": "گزارش کامل این عملیات: {desc}'",
+ "log_corrupted_md_file": "فایل فوق داده YAML مربوط به گزارش ها آسیب دیده است: '{md_file}\nخطا: {error} '",
+ "ldap_server_is_down_restart_it": "سرویس LDAP خاموش است ، سعی کنید آن را دوباره راه اندازی کنید...",
+ "ldap_server_down": "دسترسی به سرور LDAP امکان پذیر نیست",
+ "iptables_unavailable": "در اینجا نمی توانید با iptables بازی کنید. شما یا در ظرفی هستید یا هسته شما آن را پشتیبانی نمی کند",
+ "ip6tables_unavailable": "در اینجا نمی توانید با جدول های ipv6 کار کنید. شما یا در کانتینتر هستید یا هسته شما آن را پشتیبانی نمی کند",
+ "invalid_regex": "عبارت منظم نامعتبر: '{regex}'",
+ "yunohost_postinstall_end_tip": "پس از نصب کامل شد! برای نهایی کردن تنظیمات خود ، لطفاً موارد زیر را در نظر بگیرید:\n - افزودن اولین کاربر از طریق بخش \"کاربران\" webadmin (یا 'yunohost user create ' در خط فرمان) ؛\n - تشخیص مشکلات احتمالی از طریق بخش \"عیب یابی\" webadmin (یا 'yunohost diagnosis run' در خط فرمان) ؛\n - خواندن قسمت های \"نهایی کردن راه اندازی خود\" و \"آشنایی با YunoHost\" در اسناد مدیریت: https://yunohost.org/admindoc.",
+ "yunohost_not_installed": "YunoHost به درستی نصب نشده است. لطفا 'yunohost tools postinstall' را اجرا کنید",
+ "yunohost_installing": "در حال نصب YunoHost...",
+ "yunohost_configured": "YunoHost اکنون پیکربندی شده است",
+ "yunohost_already_installed": "YunoHost قبلاً نصب شده است",
+ "user_updated": "اطلاعات کاربر تغییر کرد",
+ "user_update_failed": "کاربر {user} به روز نشد: {error}",
+ "user_unknown": "کاربر ناشناس: {user}",
+ "user_home_creation_failed": "پوشه 'home' برای کاربر ایجاد نشد",
+ "user_deletion_failed": "کاربر {user} حذف نشد: {error}",
+ "user_deleted": "کاربر حذف شد",
+ "user_creation_failed": "کاربر {user} ایجاد نشد: {error}",
+ "user_created": "کاربر ایجاد شد",
+ "user_already_exists": "کاربر '{user}' در حال حاضر وجود دارد",
+ "upnp_port_open_failed": "پورت از طریق UPnP باز نشد",
+ "upnp_enabled": "UPnP روشن شد",
+ "upnp_disabled": "UPnP خاموش شد",
+ "upnp_dev_not_found": "هیچ دستگاه UPnP یافت نشد",
+ "upgrading_packages": "در حال ارتقاء بسته ها...",
+ "upgrade_complete": "ارتقا کامل شد",
+ "updating_apt_cache": "در حال واکشی و دریافت ارتقاء موجود برای بسته های سیستم...",
+ "update_apt_cache_warning": "هنگام به روز رسانی حافظه پنهان APT (مدیر بسته دبیان) مشکلی پیش آمده. در اینجا مجموعه ای از خطوط source.list موجود میباشد که ممکن است به شناسایی خطوط مشکل ساز کمک کند:\n{sourceslist}",
+ "update_apt_cache_failed": "امکان بروزرسانی حافظه پنهان APT (مدیر بسته دبیان) وجود ندارد. در اینجا مجموعه ای از خطوط source.list هست که ممکن است به شناسایی خطوط مشکل ساز کمک کند:\n{sourceslist}",
+ "unrestore_app": "{app} بازیابی نمی شود",
+ "unlimit": "بدون سهمیه",
+ "unknown_main_domain_path": "دامنه یا مسیر ناشناخته برای '{app}'. شما باید یک دامنه و یک مسیر را مشخص کنید تا بتوانید یک آدرس اینترنتی برای مجوز تعیین کنید.",
+ "unexpected_error": "مشکل غیر منتظره ای پیش آمده: {error}",
+ "unbackup_app": "{app} ذخیره نمی شود",
+ "tools_upgrade_special_packages_completed": "ارتقاء بسته YunoHost به پایان رسید\nبرای بازگرداندن خط فرمان [Enter] را فشار دهید",
+ "tools_upgrade_special_packages_explanation": "ارتقاء ویژه در پس زمینه ادامه خواهد یافت. لطفاً تا 10 دقیقه دیگر (بسته به سرعت سخت افزار) هیچ اقدام دیگری را روی سرور خود شروع نکنید. پس از این کار ، ممکن است مجبور شوید دوباره وارد webadmin شوید. گزارش ارتقاء در Tools → Log (در webadmin) یا با استفاده از 'yunohost log list' (در خط فرمان) در دسترس خواهد بود.",
+ "tools_upgrade_special_packages": "در حال ارتقاء بسته های 'special' (مربوط به yunohost)...",
+ "tools_upgrade_regular_packages_failed": "بسته ها را نمی توان ارتقا داد: {packages_list}",
+ "tools_upgrade_regular_packages": "در حال ارتقاء بسته های 'regular' (غیر مرتبط با yunohost)...",
+ "tools_upgrade_cant_unhold_critical_packages": "بسته های مهم و حیاتی را نمی توان نگه نداشت...",
+ "tools_upgrade_cant_hold_critical_packages": "بسته های مهم و حیاتی را نمی توان نگه داشت...",
+ "tools_upgrade_cant_both": "نمی توان سیستم و برنامه ها را به طور همزمان ارتقا داد",
+ "tools_upgrade_at_least_one": "لطفاً مشخص کنید 'apps' ، یا 'system'",
+ "this_action_broke_dpkg": "این اقدام dpkg/APT (مدیران بسته های سیستم) را خراب کرد... می توانید با اتصال از طریق SSH و اجرای فرمان `sudo apt install --fix -break` و/یا` sudo dpkg --configure -a` این مشکل را حل کنید.",
+ "system_username_exists": "نام کاربری قبلاً در لیست کاربران سیستم وجود دارد",
+ "system_upgraded": "سیستم ارتقا یافت",
+ "ssowat_conf_updated": "پیکربندی SSOwat به روزرسانی شد",
+ "ssowat_conf_generated": "پیکربندی SSOwat بازسازی شد",
+ "show_tile_cant_be_enabled_for_regex": "شما نمی توانید \"show_tile\" را درست فعال کنید ، چرا که آدرس اینترنتی مجوز '{permission}' یک عبارت منظم است",
+ "show_tile_cant_be_enabled_for_url_not_defined": "شما نمی توانید \"show_tile\" را در حال حاضر فعال کنید ، زیرا ابتدا باید یک آدرس اینترنتی برای مجوز '{permission}' تعریف کنید",
+ "service_unknown": "سرویس ناشناخته '{service}'",
+ "service_stopped": "سرویس '{service}' متوقف شد",
+ "service_stop_failed": "سرویس '{service}' متوقف نمی شود\n\nگزارشات اخیر سرویس: {logs}",
+ "service_started": "سرویس '{service}' شروع شد",
+ "service_start_failed": "سرویس '{service}' شروع نشد\n\nگزارشات اخیر سرویس: {logs}",
+ "service_reloaded_or_restarted": "سرویس '{service}' بارگیری یا راه اندازی مجدد شد",
+ "service_reload_or_restart_failed": "سرویس \"{service}\" بارگیری یا راه اندازی مجدد نشد\n\nگزارشات اخیر سرویس: {logs}",
+ "service_restarted": "سرویس '{service}' راه اندازی مجدد شد",
+ "service_restart_failed": "سرویس \"{service}\" راه اندازی مجدد نشد\n\nگزارشات اخیر سرویس: {logs}",
+ "service_reloaded": "سرویس '{service}' بارگیری مجدد شد",
+ "service_reload_failed": "سرویس '{service}' بارگیری نشد\n\nگزارشات اخیر سرویس: {logs}",
+ "service_removed": "سرویس '{service}' حذف شد",
+ "service_remove_failed": "سرویس '{service}' حذف نشد",
+ "service_regen_conf_is_deprecated": "فرمان 'yunohost service regen-conf' منسوخ شده است! لطفاً به جای آن از 'yunohost tools regen-conf' استفاده کنید.",
+ "service_enabled": "سرویس '{service}' اکنون بطور خودکار در هنگام بوت شدن سیستم راه اندازی می شود.",
+ "service_enable_failed": "انجام سرویس '{service}' به طور خودکار در هنگام راه اندازی امکان پذیر نیست.\n\nگزارشات اخیر سرویس: {logs}",
+ "service_disabled": "هنگام راه اندازی سیستم ، سرویس '{service}' دیگر راه اندازی نمی شود.",
+ "service_disable_failed": "نتوانست باعث شود سرویس '{service}' در هنگام راه اندازی شروع نشود.\n\nگزارشات سرویس اخیر: {logs}",
+ "service_description_yunohost-firewall": "باز و بسته شدن پورت های اتصال به سرویس ها را مدیریت می کند",
+ "service_description_yunohost-api": "تعاملات بین رابط وب YunoHost و سیستم را مدیریت می کند",
+ "service_description_ssh": "به شما امکان می دهد از راه دور از طریق ترمینال (پروتکل SSH) به سرور خود متصل شوید",
+ "service_description_slapd": "کاربران ، دامنه ها و اطلاعات مرتبط را ذخیره می کند",
+ "service_description_rspamd": "هرزنامه ها و سایر ویژگی های مربوط به ایمیل را فیلتر می کند",
+ "service_description_redis-server": "یک پایگاه داده تخصصی برای دسترسی سریع به داده ها ، صف وظیفه و ارتباط بین برنامه ها استفاده می شود",
+ "service_description_postfix": "برای ارسال و دریافت ایمیل استفاده می شود",
+ "service_description_php7.3-fpm": "برنامه های نوشته شده با PHP را با NGINX اجرا می کند",
+ "service_description_nginx": "به همه وب سایت هایی که روی سرور شما میزبانی شده اند سرویس می دهد یا دسترسی به آنها را فراهم می کند",
+ "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": "نمی توان دستور '{command}' را اجرا کرد",
+ "service_already_stopped": "سرویس '{service}' قبلاً متوقف شده است",
+ "service_already_started": "سرویس '{service}' در حال اجرا است",
+ "service_added": "سرویس '{service}' اضافه شد",
+ "service_add_failed": "سرویس '{service}' اضافه نشد",
+ "server_reboot_confirm": "سرور بلافاصله راه اندازی مجدد می شود، آیا مطمئن هستید؟ [{answers}]",
+ "server_reboot": "سرور راه اندازی مجدد می شود",
+ "server_shutdown_confirm": "آیا مطمئن هستید که سرور بلافاصله خاموش می شود؟ [{answers}]",
+ "server_shutdown": "سرور خاموش می شود",
+ "root_password_replaced_by_admin_password": "گذرواژه ریشه شما با رمز مدیریت جایگزین شده است.",
+ "root_password_desynchronized": "گذرواژه مدیریت تغییر کرد ، اما YunoHost نتوانست این را به رمز عبور ریشه منتقل کند!",
+ "restore_system_part_failed": "بخش سیستم '{part}' بازیابی و ترمیم نشد",
+ "restore_running_hooks": "در حال اجرای قلاب های ترمیم و بازیابی...",
+ "restore_running_app_script": "ترمیم و بازیابی برنامه '{app}'...",
+ "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 ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)",
+ "restore_hook_unavailable": "اسکریپت ترمیم و بازسازی برای '{part}' در سیستم شما در دسترس نیست و همچنین در بایگانی نیز وجود ندارد",
+ "restore_failed": "سیستم بازیابی نشد",
+ "restore_extracting": "استخراج فایل های مورد نیاز از بایگانی...",
+ "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}' در حال حاضر نصب شده است",
+ "regex_with_only_domain": "شما نمی توانید از عبارات منظم برای دامنه استفاده کنید، فقط برای مسیر قابل استفاده است",
+ "regex_incompatible_with_tile": "/!\\ بسته بندی کنندگان! مجوز '{permission}' show_tile را روی 'true' تنظیم کرده اند و بنابراین نمی توانید عبارت منظم آدرس اینترنتی را به عنوان URL اصلی تعریف کنید",
+ "regenconf_need_to_explicitly_specify_ssh": "پیکربندی ssh به صورت دستی تغییر یافته است ، اما شما باید صراحتاً دسته \"ssh\" را با --force برای اعمال تغییرات در واقع مشخص کنید.",
+ "regenconf_pending_applying": "در حال اعمال پیکربندی معلق برای دسته '{category}'...",
+ "regenconf_failed": "پیکربندی برای دسته (ها) بازسازی نشد: {categories}",
+ "regenconf_dry_pending_applying": "در حال بررسی پیکربندی معلق که برای دسته '{category}' اعمال می شد...",
+ "regenconf_would_be_updated": "پیکربندی برای دسته '{category}' به روز می شد",
+ "regenconf_updated": "پیکربندی برای دسته '{category}' به روز شد",
+ "regenconf_up_to_date": "پیکربندی در حال حاضر برای دسته '{category}' به روز است",
+ "regenconf_now_managed_by_yunohost": "فایل پیکربندی '{conf}' اکنون توسط YunoHost (دسته {category}) مدیریت می شود.",
+ "regenconf_file_updated": "فایل پیکربندی '{conf}' به روز شد",
+ "regenconf_file_removed": "فایل پیکربندی '{conf}' حذف شد",
+ "regenconf_file_remove_failed": "فایل پیکربندی '{conf}' حذف نشد",
+ "regenconf_file_manually_removed": "فایل پیکربندی '{conf}' به صورت دستی حذف شد، و ایجاد نخواهد شد",
+ "regenconf_file_manually_modified": "فایل پیکربندی '{conf}' به صورت دستی اصلاح شده است و به روز نمی شود",
+ "regenconf_file_kept_back": "انتظار میرفت که فایل پیکربندی '{conf}' توسط regen-conf (دسته {category}) حذف شود ، اما پس گرفته شد.",
+ "regenconf_file_copy_failed": "فایل پیکربندی جدید '{new}' در '{conf}' کپی نشد",
+ "regenconf_file_backed_up": "فایل پیکربندی '{conf}' در '{backup}' پشتیبان گیری شد",
+ "postinstall_low_rootfsspace": "فضای فایل سیستم اصلی کمتر از 10 گیگابایت است که بسیار نگران کننده است! به احتمال زیاد خیلی زود فضای دیسک شما تمام می شود! توصیه می شود حداقل 16 گیگابایت برای سیستم فایل ریشه داشته باشید. اگر می خواهید YunoHost را با وجود این هشدار نصب کنید ، فرمان نصب را مجدد با این آپشن --force-diskspace اجرا کنید",
+ "port_already_opened": "پورت {port} قبلاً برای اتصالات {ip_version} باز شده است",
+ "port_already_closed": "پورت {port} قبلاً برای اتصالات {ip_version} بسته شده است",
+ "permission_require_account": "مجوز {permission} فقط برای کاربران دارای حساب کاربری منطقی است و بنابراین نمی تواند برای بازدیدکنندگان فعال شود.",
+ "permission_protected": "مجوز {permission} محافظت می شود. شما نمی توانید گروه بازدیدکنندگان را از/به این مجوز اضافه یا حذف کنید.",
+ "permission_updated": "مجوز '{permission}' به روز شد",
+ "permission_update_failed": "مجوز '{permission}' به روز نشد: {error}",
+ "permission_not_found": "مجوز '{permission}' پیدا نشد",
+ "permission_deletion_failed": "اجازه '{permission}' حذف نشد: {error}",
+ "permission_deleted": "مجوز '{permission}' حذف شد",
+ "permission_cant_add_to_all_users": "مجوز {permission} را نمی توان به همه کاربران اضافه کرد.",
+ "permission_currently_allowed_for_all_users": "این مجوز در حال حاضر به همه کاربران علاوه بر آن گروه های دیگر نیز اعطا شده. احتمالاً بخواهید مجوز 'all_users' را حذف کنید یا سایر گروه هایی را که در حال حاضر مجوز به آنها اعطا شده است را هم حذف کنید."
+}
\ No newline at end of file
diff --git a/locales/fi.json b/locales/fi.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/locales/fi.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/locales/fr.json b/locales/fr.json
index b65268fb7..123270bd6 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -1,51 +1,51 @@
{
- "action_invalid": "Action '{action:s}' incorrecte",
- "admin_password": "Mot de passe d’administration",
+ "action_invalid": "Action '{action}' incorrecte",
+ "admin_password": "Mot de passe d'administration",
"admin_password_change_failed": "Impossible de changer le mot de passe",
- "admin_password_changed": "Le mot de passe d’administration a été modifié",
- "app_already_installed": "{app:s} est déjà installé",
- "app_argument_choice_invalid": "Choix invalide pour le paramètre '{name:s}', il doit être l’un de {choices:s}",
- "app_argument_invalid": "Valeur invalide pour le paramètre '{name:s}' : {error:s}",
- "app_argument_required": "Le paramètre '{name:s}' est requis",
- "app_extraction_failed": "Impossible d’extraire les fichiers d’installation",
- "app_id_invalid": "Identifiant d’application invalide",
- "app_install_files_invalid": "Fichiers d’installation incorrects",
- "app_manifest_invalid": "Manifeste d’application incorrect : {error}",
- "app_not_correctly_installed": "{app:s} semble être mal installé",
- "app_not_installed": "Nous n’avons pas trouvé {app:s} dans la liste des applications installées : {all_apps}",
- "app_not_properly_removed": "{app:s} n’a pas été supprimé correctement",
- "app_removed": "{app:s} supprimé",
+ "admin_password_changed": "Le mot de passe d'administration a été modifié",
+ "app_already_installed": "{app} est déjà installé",
+ "app_argument_choice_invalid": "Choisissez une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})",
+ "app_argument_invalid": "Valeur invalide pour le paramètre '{name}' : {error}",
+ "app_argument_required": "Le paramètre '{name}' est requis",
+ "app_extraction_failed": "Impossible d'extraire les fichiers d'installation",
+ "app_id_invalid": "Identifiant d'application invalide",
+ "app_install_files_invalid": "Fichiers d'installation incorrects",
+ "app_manifest_invalid": "Manifeste d'application incorrect : {error}",
+ "app_not_correctly_installed": "{app} semble être mal installé",
+ "app_not_installed": "Nous n'avons pas trouvé {app} dans la liste des applications installées : {all_apps}",
+ "app_not_properly_removed": "{app} n'a pas été supprimé correctement",
+ "app_removed": "{app} désinstallé",
"app_requirements_checking": "Vérification des paquets requis pour {app}...",
"app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}",
- "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l’URL est-elle correcte ?",
+ "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l'URL est-elle correcte ?",
"app_unknown": "Application inconnue",
- "app_unsupported_remote_type": "Ce type de commande à distance utilisé pour cette application n’est pas supporté",
- "app_upgrade_failed": "Impossible de mettre à jour {app:s} : {error}",
- "app_upgraded": "{app:s} mis à jour",
- "ask_email": "Adresse de courriel",
+ "app_unsupported_remote_type": "Ce type de commande à distance utilisé pour cette application n'est pas supporté",
+ "app_upgrade_failed": "Impossible de mettre à jour {app} : {error}",
+ "app_upgraded": "{app} mis à jour",
"ask_firstname": "Prénom",
"ask_lastname": "Nom",
"ask_main_domain": "Domaine principal",
- "ask_new_admin_password": "Nouveau mot de passe d’administration",
+ "ask_new_admin_password": "Nouveau mot de passe d'administration",
"ask_password": "Mot de passe",
- "backup_app_failed": "Impossible de sauvegarder {app:s}",
- "backup_archive_app_not_found": "{app:s} n’a pas été trouvée dans l’archive de la sauvegarde",
+ "backup_app_failed": "Impossible de sauvegarder {app}",
+ "backup_archive_app_not_found": "{app} n'a pas été trouvée dans l'archive de la sauvegarde",
"backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà.",
- "backup_archive_name_unknown": "L’archive locale de sauvegarde nommée '{name:s}' est inconnue",
- "backup_archive_open_failed": "Impossible d’ouvrir l’archive de la sauvegarde",
+ "backup_archive_name_unknown": "L'archive locale de sauvegarde nommée '{name}' est inconnue",
+ "backup_archive_open_failed": "Impossible d'ouvrir l'archive de la sauvegarde",
"backup_cleaning_failed": "Impossible de nettoyer le dossier temporaire de sauvegarde",
"backup_created": "Sauvegarde terminée",
- "backup_creation_failed": "Impossible de créer l’archive de la sauvegarde",
- "backup_delete_error": "Impossible de supprimer '{path:s}'",
+ "backup_creation_failed": "Impossible de créer l'archive de la sauvegarde",
+ "backup_delete_error": "Impossible de supprimer '{path}'",
"backup_deleted": "La sauvegarde a été supprimée",
- "backup_hook_unknown": "Script de sauvegarde '{hook:s}' inconnu",
- "backup_invalid_archive": "Archive de sauvegarde invalide",
- "backup_nothings_done": "Il n’y a rien à sauvegarder",
+ "backup_hook_unknown": "Script de sauvegarde '{hook}' inconnu",
+ "backup_nothings_done": "Il n'y a rien à sauvegarder",
"backup_output_directory_forbidden": "Choisissez un répertoire de destination différent. Les sauvegardes ne peuvent pas être créées dans les sous-dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
- "backup_output_directory_not_empty": "Le répertoire de destination n’est pas vide",
+ "backup_output_directory_not_empty": "Le répertoire de destination n'est pas vide",
"backup_output_directory_required": "Vous devez spécifier un dossier de destination pour la sauvegarde",
- "backup_running_hooks": "Exécution des scripts de sauvegarde...",
- "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application personnalisée {app:s}",
+ "backup_running_hooks": "Exécution des scripts de sauvegarde ...",
+ "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application personnalisée {app}",
+ "disk_space_not_sufficient_install": "Il ne reste pas assez d'espace disque pour installer cette application",
+ "disk_space_not_sufficient_update": "Il ne reste pas assez d'espace disque pour mettre à jour cette application",
"domain_cert_gen_failed": "Impossible de générer le certificat",
"domain_created": "Le domaine a été créé",
"domain_creation_failed": "Impossible de créer le domaine {domain} : {error}",
@@ -54,45 +54,35 @@
"domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS",
"domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu",
"domain_exists": "Le domaine existe déjà",
- "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nAfin de pouvoir procéder à la suppression du domaine, vous devez préalablement :\n- soit désinstaller toutes ces applications avec la commande 'yunohost app remove nom-de-l-application' ;\n- soit déplacer toutes ces applications vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application'",
- "domain_unknown": "Domaine inconnu",
+ "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nVeuillez les désinstaller avec la commande 'yunohost app remove nom-de-l-application' ou les déplacer vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application' avant de procéder à la suppression du domaine",
"done": "Terminé",
- "downloading": "Téléchargement en cours …",
- "dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été créée",
- "dyndns_cron_remove_failed": "Impossible de supprimer la tâche cron DynDNS parce que : {error}",
- "dyndns_cron_removed": "La tâche cron pour le domaine DynDNS a été enlevée",
- "dyndns_ip_update_failed": "Impossible de mettre à jour l’adresse IP sur le domaine DynDNS",
+ "downloading": "Téléchargement en cours...",
+ "dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS",
"dyndns_ip_updated": "Mise à jour de votre IP pour le domaine DynDNS",
"dyndns_key_generating": "Génération de la clé DNS..., cela peut prendre un certain temps.",
"dyndns_key_not_found": "Clé DNS introuvable pour le domaine",
"dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS",
"dyndns_registered": "Domaine DynDNS enregistré",
- "dyndns_registration_failed": "Impossible d’enregistrer le domaine DynDNS : {error:s}",
- "dyndns_unavailable": "Le domaine {domain:s} est indisponible.",
- "executing_command": "Exécution de la commande '{command:s}'...",
- "executing_script": "Exécution du script '{script:s}'...",
+ "dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error}",
+ "dyndns_unavailable": "Le domaine {domain} est indisponible.",
"extracting": "Extraction en cours...",
- "field_invalid": "Champ incorrect : '{:s}'",
+ "field_invalid": "Champ incorrect : '{}'",
"firewall_reload_failed": "Impossible de recharger le pare-feu",
"firewall_reloaded": "Pare-feu rechargé",
"firewall_rules_cmd_failed": "Certaines commandes de règles de pare-feu ont échoué. Plus d'informations dans le journal.",
- "hook_exec_failed": "Échec de l’exécution du script : {path:s}",
- "hook_exec_not_terminated": "L’exécution du script {path:s} ne s’est pas terminée correctement",
+ "hook_exec_failed": "Échec de l'exécution du script : {path}",
+ "hook_exec_not_terminated": "L'exécution du script {path} ne s'est pas terminée correctement",
"hook_list_by_invalid": "Propriété invalide pour lister les actions par celle-ci",
- "hook_name_unknown": "Nom de l’action '{name:s}' inconnu",
+ "hook_name_unknown": "Nom de l'action '{name}' inconnu",
"installation_complete": "Installation terminée",
- "installation_failed": "Quelque chose s’est mal passé lors de l’installation",
"ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge",
"iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge",
- "ldap_initialized": "L’annuaire LDAP a été initialisé",
- "mail_alias_remove_failed": "Impossible de supprimer l’alias de courriel '{mail:s}'",
- "mail_domain_unknown": "Le domaine '{domain:s}' de cette adresse de courriel n’est pas valide. Merci d’utiliser un domaine administré par ce serveur.",
- "mail_forward_remove_failed": "Impossible de supprimer le courriel de transfert '{mail:s}'",
+ "mail_alias_remove_failed": "Impossible de supprimer l'alias mail '{mail}'",
+ "mail_domain_unknown": "Le domaine '{domain}' de cette adresse email n'est pas valide. Merci d'utiliser un domaine administré par ce serveur.",
+ "mail_forward_remove_failed": "Impossible de supprimer l'email de transfert '{mail}'",
"main_domain_change_failed": "Impossible de modifier le domaine principal",
"main_domain_changed": "Le domaine principal a été modifié",
- "no_internet_connection": "Le serveur n’est pas connecté à Internet",
- "not_enough_disk_space": "L’espace disque est insuffisant sur '{path:s}'",
- "package_unknown": "Le paquet '{pkgname}' est inconnu",
+ "not_enough_disk_space": "L'espace disque est insuffisant sur '{path}'",
"packages_upgrade_failed": "Impossible de mettre à jour tous les paquets",
"pattern_backup_archive_name": "Doit être un nom de fichier valide avec un maximum de 30 caractères, et composé de caractères alphanumériques et -_. uniquement",
"pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.fr)",
@@ -100,194 +90,174 @@
"pattern_firstname": "Doit être un prénom valide",
"pattern_lastname": "Doit être un nom valide",
"pattern_mailbox_quota": "Doit avoir une taille suffixée avec b/k/M/G/T ou 0 pour désactiver le quota",
- "pattern_password": "Doit être composé d’au moins 3 caractères",
+ "pattern_password": "Doit être composé d'au moins 3 caractères",
"pattern_port_or_range": "Doit être un numéro de port valide compris entre 0 et 65535, ou une gamme de ports (exemple : 100:200)",
- "pattern_positive_number": "Doit être un nombre positif",
"pattern_username": "Doit être composé uniquement de caractères alphanumériques minuscules et de tirets bas (aussi appelé tiret du 8 ou underscore)",
- "port_already_closed": "Le port {port:d} est déjà fermé pour les connexions {ip_version:s}",
- "port_already_opened": "Le port {port:d} est déjà ouvert pour les connexions {ip_version:s}",
- "restore_already_installed_app": "Une application est déjà installée avec l’identifiant '{app:s}'",
- "restore_app_failed": "Impossible de restaurer '{app:s}'",
+ "port_already_closed": "Le port {port} est déjà fermé pour les connexions {ip_version}",
+ "port_already_opened": "Le port {port} est déjà ouvert pour les connexions {ip_version}",
+ "restore_already_installed_app": "Une application est déjà installée avec l'identifiant '{app}'",
+ "app_restore_failed": "Impossible de restaurer {app} : {error}",
"restore_cleaning_failed": "Impossible de nettoyer le dossier temporaire de restauration",
"restore_complete": "Restauration terminée",
- "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]",
+ "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers}]",
"restore_failed": "Impossible de restaurer le système",
- "restore_hook_unavailable": "Le script de restauration '{part:s}' n’est pas disponible sur votre système, et ne l’est pas non plus dans l’archive",
- "restore_nothings_done": "Rien n’a été restauré",
- "restore_running_app_script": "Exécution du script de restauration de l’application '{app:s}'…",
- "restore_running_hooks": "Exécution des scripts de restauration…",
- "service_add_failed": "Impossible d’ajouter le service '{service:s}'",
- "service_added": "Le service '{service:s}' a été ajouté",
- "service_already_started": "Le service '{service:s}' est déjà en cours d’exécution",
- "service_already_stopped": "Le service '{service:s}' est déjà arrêté",
- "service_cmd_exec_failed": "Impossible d’exécuter la commande '{command:s}'",
- "service_disable_failed": "Impossible de ne pas lancer le service « {service:s} » au démarrage.\n\nJournaux récents du service : {logs:s}",
- "service_disabled": "Le service « {service:s} » ne sera plus lancé au démarrage du système.",
- "service_enable_failed": "Impossible de lancer automatiquement le service « {service:s} » au démarrage.\n\nJournaux récents du service : {logs:s}",
- "service_enabled": "Le service « {service:s} » sera désormais lancé automatiquement au démarrage du système.",
- "service_remove_failed": "Impossible de supprimer le service '{service:s}'",
- "service_removed": "Le service « {service:s} » a été supprimé",
- "service_start_failed": "Impossible de démarrer le service '{service:s}'\n\nJournaux historisés récents : {logs:s}",
- "service_started": "Le service « {service:s} » a été démarré",
- "service_stop_failed": "Impossible d’arrêter le service '{service:s}'\n\nJournaux récents de service : {logs:s}",
- "service_stopped": "Le service « {service:s} » a été arrêté",
- "service_unknown": "Le service '{service:s}' est inconnu",
+ "restore_hook_unavailable": "Le script de restauration '{part}' n'est pas disponible sur votre système, et ne l'est pas non plus dans l'archive",
+ "restore_nothings_done": "Rien n'a été restauré",
+ "restore_running_app_script": "Exécution du script de restauration de l'application '{app}'...",
+ "restore_running_hooks": "Exécution des scripts de restauration...",
+ "service_add_failed": "Impossible d'ajouter le service '{service}'",
+ "service_added": "Le service '{service}' a été ajouté",
+ "service_already_started": "Le service '{service}' est déjà en cours d'exécution",
+ "service_already_stopped": "Le service '{service}' est déjà arrêté",
+ "service_cmd_exec_failed": "Impossible d'exécuter la commande '{command}'",
+ "service_disable_failed": "Impossible de ne pas lancer le service '{service}' au démarrage.\n\nJournaux récents du service : {logs}",
+ "service_disabled": "Le service '{service}' ne sera plus lancé au démarrage du système.",
+ "service_enable_failed": "Impossible de lancer automatiquement le service '{service}' au démarrage.\n\nJournaux récents du service : {logs}",
+ "service_enabled": "Le service '{service}' sera désormais lancé automatiquement au démarrage du système.",
+ "service_remove_failed": "Impossible de supprimer le service '{service}'",
+ "service_removed": "Le service '{service}' a été supprimé",
+ "service_start_failed": "Impossible de démarrer le service '{service}'\n\nJournaux historisés récents : {logs}",
+ "service_started": "Le service '{service}' a été démarré",
+ "service_stop_failed": "Impossible d'arrêter le service '{service}'\n\nJournaux récents de service : {logs}",
+ "service_stopped": "Le service '{service}' a été arrêté",
+ "service_unknown": "Le service '{service}' est inconnu",
"ssowat_conf_generated": "La configuration de SSOwat a été regénérée",
"ssowat_conf_updated": "La configuration de SSOwat a été mise à jour",
"system_upgraded": "Système mis à jour",
- "system_username_exists": "Ce nom d’utilisateur existe déjà dans les utilisateurs système",
- "unbackup_app": "'{app:s}' ne sera pas sauvegardée",
+ "system_username_exists": "Ce nom d'utilisateur existe déjà dans les utilisateurs système",
+ "unbackup_app": "'{app}' ne sera pas sauvegardée",
"unexpected_error": "Une erreur inattendue est survenue : {error}",
"unlimit": "Pas de quota",
- "unrestore_app": "'{app:s}' ne sera pas restaurée",
+ "unrestore_app": "'{app}' ne sera pas restaurée",
"updating_apt_cache": "Récupération des mises à jour disponibles pour les paquets du système...",
"upgrade_complete": "Mise à jour terminée",
"upgrading_packages": "Mise à jour des paquets en cours...",
- "upnp_dev_not_found": "Aucun périphérique compatible UPnP n’a été trouvé",
- "upnp_disabled": "UPnP désactivé",
- "upnp_enabled": "UPnP 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:s} est inconnu",
- "user_update_failed": "Impossible de mettre à jour l’utilisateur {user} : {error}",
- "user_updated": "L’utilisateur a été modifié",
+ "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 '{home}' 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é",
"yunohost_already_installed": "YunoHost est déjà installé",
- "yunohost_ca_creation_failed": "Impossible de créer l’autorité de certification",
"yunohost_configured": "YunoHost est maintenant configuré",
- "yunohost_installing": "L’installation de YunoHost est en cours...",
- "yunohost_not_installed": "YunoHost n’est pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'",
- "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de vouloir remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner cela)",
- "certmanager_domain_unknown": "Domaine {domain:s} inconnu",
- "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} n’est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force pour cela)",
- "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l’activation du nouveau certificat pour {domain:s} a échoué...",
- "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} n’est pas émis par Let’s Encrypt. Impossible de le renouveler automatiquement !",
- "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} n’est pas sur le point d’expirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)",
- "certmanager_domain_http_not_working": "Le domaine {domain:s} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)",
- "certmanager_error_no_A_record": "Aucun enregistrement DNS 'A' n’a été trouvé pour {domain:s}. Vous devez faire pointer votre nom de domaine vers votre machine pour être en mesure d’installer un certificat Let’s Encrypt ! (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "L'enregistrement DNS du domaine {domain:s} est différent de l’adresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section diagnostic. Si vous avez récemment modifié votre enregistrement 'A', veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)",
- "certmanager_cannot_read_cert": "Quelque chose s’est mal passé lors de la tentative d’ouverture du certificat actuel pour le domaine {domain:s} (fichier : {file:s}), la cause est : {reason:s}",
- "certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine « {domain:s} »",
- "certmanager_cert_install_success": "Le certificat Let’s Encrypt est maintenant installé pour le domaine « {domain:s} »",
- "certmanager_cert_renew_success": "Certificat Let’s Encrypt renouvelé pour le domaine '{domain:s}'",
+ "yunohost_installing": "L'installation de YunoHost est en cours...",
+ "yunohost_not_installed": "YunoHost n'est pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'",
+ "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de vouloir remplacer un certificat correct et valide pour le domaine {domain} ! (Utilisez --force pour contourner cela)",
+ "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain} n'est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force pour cela)",
+ "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain} a échoué...",
+ "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain} n'est pas émis par Let's Encrypt. Impossible de le renouveler automatiquement !",
+ "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain} n'est pas sur le point d'expirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)",
+ "certmanager_domain_http_not_working": "Le domaine {domain} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "Les enregistrements DNS du domaine '{domain}' sont différents de l'adresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles)",
+ "certmanager_cannot_read_cert": "Quelque chose s'est mal passé lors de la tentative d'ouverture du certificat actuel pour le domaine {domain} (fichier : {file}), la cause est : {reason}",
+ "certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine '{domain}'",
+ "certmanager_cert_install_success": "Le certificat Let's Encrypt est maintenant installé pour le domaine '{domain}'",
+ "certmanager_cert_renew_success": "Certificat Let's Encrypt renouvelé pour le domaine '{domain}'",
"certmanager_cert_signing_failed": "Impossible de signer le nouveau certificat",
- "certmanager_no_cert_file": "Impossible de lire le fichier du certificat pour le domaine {domain:s} (fichier : {file:s})",
- "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour le défi ACME : le fichier de configuration NGINX {filepath:s} est en conflit et doit être préalablement retiré",
- "certmanager_hit_rate_limit": "Trop de certificats ont déjà été émis récemment pour ce même ensemble de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails sur les ratios et limitations",
- "ldap_init_failed_to_create_admin": "L’initialisation de l’annuaire LDAP n’a pas réussi à créer l’utilisateur admin",
- "domain_cannot_remove_main": "Vous ne pouvez pas supprimer '{domain:s}' car il s’agit du domaine principal. Vous devez d’abord définir un autre domaine comme domaine principal à l’aide de 'yunohost domain main-domain -n ', voici la liste des domaines candidats : {other_domains:s}",
- "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})",
- "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})",
- "mailbox_used_space_dovecot_down": "Le service de courriel Dovecot doit être démarré si vous souhaitez voir l’espace disque occupé par la messagerie",
+ "certmanager_no_cert_file": "Impossible de lire le fichier du certificat pour le domaine {domain} (fichier : {file})",
+ "certmanager_hit_rate_limit": "Trop de certificats ont déjà été émis récemment pour ce même ensemble de domaines {domain}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails sur les ratios et limitations",
+ "domain_cannot_remove_main": "Vous ne pouvez pas supprimer '{domain}' car il s'agit du domaine principal. Vous devez d'abord définir un autre domaine comme domaine principal à l'aide de 'yunohost domain main-domain -n ', voici la liste des domaines candidats : {other_domains}",
+ "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l'autorité du certificat auto-signé est introuvable (fichier : {file})",
+ "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:s})",
- "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`.",
- "certmanager_http_check_timeout": "Expiration du délai lorsque le serveur a essayé de se contacter lui-même via HTTP en utilisant l’adresse IP public {ip:s} du domaine {domain:s}. Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.",
- "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation ou le renouvellement du certificat a été annulé. Veuillez réessayer plus tard.",
- "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).",
- "yunohost_ca_creation_success": "L'autorité de certification locale a été créée.",
- "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`.",
- "app_change_url_failed_nginx_reload": "Le redémarrage de Nginx a échoué. Voici la sortie de 'nginx -t' :\n{nginx_errors:s}",
- "app_change_url_identical_domains": "L’ancien et le nouveau couple domaine/chemin_de_l’URL sont identiques pour ('{domain:s}{path:s}'), rien à faire.",
- "app_change_url_no_script": "L’application '{app_name:s}' ne prend pas encore en charge le changement d’URL. Vous devriez peut-être la mettre à jour.",
- "app_change_url_success": "L’URL de l’application {app:s} a été changée en {domain:s}{path:s}",
- "app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante :\n{apps:s}",
- "app_already_up_to_date": "{app:s} est déjà à jour",
- "global_settings_bad_choice_for_enum": "Valeur du paramètre {setting:s} incorrecte. Reçu : {choice:s}, mais les valeurs possibles sont : {available_choices:s}",
- "global_settings_bad_type_for_setting": "Le type du paramètre {setting:s} est incorrect. Reçu {received_type:s} alors que {expected_type:s} était attendu",
- "global_settings_cant_open_settings": "Échec de l’ouverture du ficher de configurations car : {reason:s}",
- "global_settings_cant_write_settings": "Échec d’écriture du fichier de configurations car : {reason:s}",
- "global_settings_key_doesnt_exists": "La clef '{settings_key:s}' n’existe pas dans les configurations générales, vous pouvez voir toutes les clefs disponibles en saisissant 'yunohost settings list'",
- "global_settings_reset_success": "Vos configurations précédentes ont été sauvegardées dans {path:s}",
- "global_settings_setting_example_bool": "Exemple d’option booléenne",
- "global_settings_setting_example_int": "Exemple d’option de type entier",
- "global_settings_setting_example_string": "Exemple d’option de type chaîne",
- "global_settings_setting_example_enum": "Exemple d’option de type énumération",
- "global_settings_unknown_type": "Situation inattendue : la configuration {setting:s} semble avoir le type {unknown_type:s} mais celui-ci n’est pas pris en charge par le système.",
- "global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key:s}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json",
+ "backup_archive_broken_link": "Impossible d'accéder à l'archive 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`.",
+ "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.",
+ "app_change_url_success": "L'URL de l'application {app} a été changée en {domain}{path}",
+ "app_location_unavailable": "Cette URL n'est pas disponible ou est en conflit avec une application existante :\n{apps}",
+ "app_already_up_to_date": "{app} est déjà à jour",
+ "global_settings_bad_choice_for_enum": "Valeur du paramètre {setting} incorrecte. Reçu : {choice}, mais les valeurs possibles sont : {available_choices}",
+ "global_settings_bad_type_for_setting": "Le type du paramètre {setting} est incorrect. Reçu {received_type} alors que {expected_type} était attendu",
+ "global_settings_cant_open_settings": "Échec de l'ouverture du ficher de configurations car : {reason}",
+ "global_settings_cant_write_settings": "Échec d'écriture du fichier de configurations car : {reason}",
+ "global_settings_key_doesnt_exists": "La clef '{settings_key}' n'existe pas dans les configurations générales, vous pouvez voir toutes les clefs disponibles en saisissant 'yunohost settings list'",
+ "global_settings_reset_success": "Vos configurations précédentes ont été sauvegardées dans {path}",
+ "global_settings_unknown_type": "Situation inattendue : la configuration {setting} semble avoir le type {unknown_type} mais celui-ci n'est pas pris en charge par le système.",
+ "global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json",
"backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter",
- "backup_applying_method_tar": "Création de l’archive TAR de la sauvegarde...",
- "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder...",
- "backup_applying_method_borg": "Envoi de tous les fichiers à sauvegarder dans le répertoire borg-backup...",
- "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method:s}'...",
- "backup_archive_system_part_not_available": "La partie '{part:s}' du système n’est pas disponible dans cette sauvegarde",
- "backup_archive_writing_error": "Impossible d’ajouter des fichiers '{source:s}' (nommés dans l’archive : '{dest:s}') à sauvegarder dans l’archive compressée '{archive:s}'",
- "backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size:s}Mo temporairement ? (Cette méthode est utilisée car certains fichiers n’ont pas pu être préparés avec une méthode plus efficace.)",
- "backup_borg_not_implemented": "La méthode de sauvegarde Borg n’est pas encore implémentée",
- "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l’archive décompressée",
- "backup_copying_to_organize_the_archive": "Copie de {size:s} Mo pour organiser l’archive",
+ "backup_applying_method_tar": "Création de l'archive TAR de la sauvegarde ...",
+ "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder ...",
+ "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method}' ...",
+ "backup_archive_system_part_not_available": "La partie '{part}' du système n'est pas disponible dans cette sauvegarde",
+ "backup_archive_writing_error": "Impossible d'ajouter des fichiers '{source}' (nommés dans l'archive : '{dest}') à sauvegarder dans l'archive compressée '{archive}'",
+ "backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size}Mo temporairement ? (Cette méthode est utilisée car certains fichiers n'ont pas pu être préparés avec une méthode plus efficace.)",
+ "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l'archive décompressée",
+ "backup_copying_to_organize_the_archive": "Copie de {size} Mo pour organiser l'archive",
"backup_csv_creation_failed": "Impossible de créer le fichier CSV nécessaire à la restauration",
- "backup_csv_addition_failed": "Impossible d’ajouter des fichiers à sauvegarder dans le fichier CSV",
- "backup_custom_backup_error": "Échec de la méthode de sauvegarde personnalisée à l’étape 'backup'",
- "backup_custom_mount_error": "Échec de la méthode de sauvegarde personnalisée à l’étape 'mount'",
- "backup_no_uncompress_archive_dir": "Ce dossier d’archive décompressée n’existe pas",
- "backup_method_tar_finished": "L’archive TAR de la sauvegarde a été créée",
+ "backup_csv_addition_failed": "Impossible d'ajouter des fichiers à sauvegarder dans le fichier CSV",
+ "backup_custom_backup_error": "Échec de la méthode de sauvegarde personnalisée à l'étape 'backup'",
+ "backup_custom_mount_error": "Échec de la méthode de sauvegarde personnalisée à l'étape 'mount'",
+ "backup_no_uncompress_archive_dir": "Ce dossier d'archive décompressée n'existe pas",
+ "backup_method_tar_finished": "L'archive TAR de la sauvegarde a été créée",
"backup_method_copy_finished": "La copie de la sauvegarde est terminée",
- "backup_method_borg_finished": "La sauvegarde dans Borg est terminée",
- "backup_method_custom_finished": "La méthode de sauvegarde personnalisée '{method:s}' est terminée",
- "backup_system_part_failed": "Impossible de sauvegarder la partie '{part:s}' du système",
- "backup_unable_to_organize_files": "Impossible d’utiliser la méthode rapide pour organiser les fichiers dans l’archive",
- "backup_with_no_backup_script_for_app": "L’application {app:s} n’a pas de script de sauvegarde. Ignorer.",
- "backup_with_no_restore_script_for_app": "{app:s} n’a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.",
- "global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage car : {reason:s}",
+ "backup_method_custom_finished": "La méthode de sauvegarde personnalisée '{method}' est terminée",
+ "backup_system_part_failed": "Impossible de sauvegarder la partie '{part}' du système",
+ "backup_unable_to_organize_files": "Impossible d'utiliser la méthode rapide pour organiser les fichiers dans l'archive",
+ "backup_with_no_backup_script_for_app": "L'application {app} n'a pas de script de sauvegarde. Ignorer.",
+ "backup_with_no_restore_script_for_app": "{app} n'a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.",
+ "global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage car : {reason}",
"restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire",
- "restore_extracting": "Extraction des fichiers nécessaires depuis l’archive…",
- "restore_may_be_not_enough_disk_space": "Votre système ne semble pas avoir suffisamment d’espace (libre : {free_space:d} B, espace nécessaire : {needed_space:d} B, marge de sécurité : {margin:d} B)",
- "restore_not_enough_disk_space": "Espace disponible insuffisant (L’espace libre est de {free_space:d} octets. Le besoin d’espace nécessaire est de {needed_space:d} octets. En appliquant une marge de sécurité, la quantité d’espace nécessaire est de {margin:d} octets)",
- "restore_system_part_failed": "Impossible de restaurer la partie '{part:s}' du système",
- "backup_couldnt_bind": "Impossible de lier {src:s} avec {dest:s}.",
- "domain_dns_conf_is_just_a_recommendation": "Cette page montre la configuration *recommandée*. Elle ne configure *pas* le DNS pour vous. Il est de votre responsabilité que de configurer votre zone DNS chez votre fournisseur/registrar DNS avec cette recommandation.",
- "migrations_cant_reach_migration_file": "Impossible d’accéder aux fichiers de migration via le chemin '%s'",
+ "restore_extracting": "Extraction des fichiers nécessaires depuis l'archive...",
+ "restore_may_be_not_enough_disk_space": "Votre système ne semble pas avoir suffisamment d'espace (libre : {free_space} B, espace nécessaire : {needed_space} B, marge de sécurité : {margin} B)",
+ "restore_not_enough_disk_space": "Espace disponible insuffisant (L'espace libre est de {free_space} octets. Le besoin d'espace nécessaire est de {needed_space} octets. En appliquant une marge de sécurité, la quantité d'espace nécessaire est de {margin} octets)",
+ "restore_system_part_failed": "Impossible de restaurer la partie '{part}' du système",
+ "backup_couldnt_bind": "Impossible de lier {src} avec {dest}.",
+ "domain_dns_conf_is_just_a_recommendation": "Cette commande vous montre la configuration *recommandée*. Elle ne configure pas le DNS pour vous. Il est de votre ressort de configurer votre zone DNS chez votre registrar/fournisseur conformément à cette recommandation.",
+ "migrations_cant_reach_migration_file": "Impossible d'accéder aux fichiers de migration via le chemin '%s'",
"migrations_loading_migration": "Chargement de la migration {id}...",
- "migrations_migration_has_failed": "La migration {id} a échoué avec l’exception {exception} : annulation",
+ "migrations_migration_has_failed": "La migration {id} a échoué avec l'exception {exception} : annulation",
"migrations_no_migrations_to_run": "Aucune migration à lancer",
"migrations_skip_migration": "Ignorer et passer la migration {id}...",
- "server_shutdown": "Le serveur va s’éteindre",
- "server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers:s}]",
+ "server_shutdown": "Le serveur va s'éteindre",
+ "server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers}]",
"server_reboot": "Le serveur va redémarrer",
- "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]",
- "app_upgrade_some_app_failed": "Certaines applications n’ont pas été mises à jour",
- "dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.",
- "dyndns_domain_not_provided": "Le fournisseur DynDNS {provider:s} ne peut pas fournir le domaine {domain:s}.",
- "app_make_default_location_already_used": "Impossible de configurer l’application '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l'application '{other_app}'",
+ "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers}]",
+ "app_upgrade_some_app_failed": "Certaines applications n'ont pas été mises à jour",
+ "dyndns_could_not_check_provide": "Impossible de vérifier si {provider} peut fournir {domain}.",
+ "dyndns_domain_not_provided": "Le fournisseur DynDNS {provider} ne peut pas fournir le domaine {domain}.",
+ "app_make_default_location_already_used": "Impossible de configurer l'application '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l'application '{other_app}'",
"app_upgrade_app_name": "Mise à jour de {app}...",
- "backup_output_symlink_dir_broken": "Votre répertoire d’archivage '{path:s}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.",
+ "backup_output_symlink_dir_broken": "Votre répertoire d'archivage '{path}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.",
"migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.",
- "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans l’interface admin, ou lancer `yunohost tools migrations run`.",
- "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer.",
- "service_description_avahi-daemon": "Vous permet d’atteindre votre serveur en utilisant « yunohost.local » sur votre réseau local",
+ "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans la webadmin, ou lancer `yunohost tools migrations run`.",
+ "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l'option --accept-disclaimer.",
+ "service_description_yunomdns": "Vous permet d'atteindre votre serveur en utilisant 'yunohost.local' sur votre réseau local",
"service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)",
- "service_description_dovecot": "Permet aux clients de messagerie d’accéder/récupérer les courriels (via IMAP et POP3)",
- "service_description_fail2ban": "Protège contre les attaques brute-force et autres types d’attaques venant d’Internet",
+ "service_description_dovecot": "Permet aux clients de messagerie d'accéder/récupérer les emails (via IMAP et POP3)",
+ "service_description_fail2ban": "Protège contre les attaques brute-force et autres types d'attaques venant d'Internet",
"service_description_metronome": "Gère les comptes de messagerie instantanée XMPP",
"service_description_mysql": "Stocke les données des applications (bases de données SQL)",
- "service_description_nginx": "Sert ou permet l’accès à tous les sites web hébergés sur votre serveur",
- "service_description_nslcd": "Gère la connexion en ligne de commande des utilisateurs YunoHost",
- "service_description_postfix": "Utilisé pour envoyer et recevoir des courriels",
- "service_description_redis-server": "Une base de données spécialisée utilisée pour l’accès rapide aux données, les files d’attentes et la communication entre les programmes",
- "service_description_rspamd": "Filtre le pourriel, et d’autres fonctionnalités liées au courriel",
+ "service_description_nginx": "Sert ou permet l'accès à tous les sites web hébergés sur votre serveur",
+ "service_description_postfix": "Utilisé pour envoyer et recevoir des emails",
+ "service_description_redis-server": "Une base de données spécialisée utilisée pour l'accès rapide aux données, les files d'attentes et la communication entre les programmes",
+ "service_description_rspamd": "Filtre le pourriel, et d'autres fonctionnalités liées aux emails",
"service_description_slapd": "Stocke les utilisateurs, domaines et leurs informations liées",
"service_description_ssh": "Vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)",
- "service_description_yunohost-api": "Permet les interactions entre l’interface web de YunoHost et le système",
- "service_description_yunohost-firewall": "Gère l’ouverture et la fermeture des ports de connexion aux services",
- "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l’utiliser à moins que vous ne sachiez ce que vous faites.",
- "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}",
- "log_category_404": "Le journal de la catégorie '{category}' n’existe pas",
+ "service_description_yunohost-api": "Permet les interactions entre l'interface web de YunoHost et le système",
+ "service_description_yunohost-firewall": "Gère l'ouverture et la fermeture des ports de connexion aux services",
+ "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faites.",
+ "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}",
"log_link_to_log": "Journal complet de cette opération : ' {desc} '",
- "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}{name}'",
- "log_link_to_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en cliquant ici",
- "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge PHP 7, vous pourriez ne plus pouvoir restaurer vos applications PHP (cause : {error:s})",
- "log_help_to_get_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en utilisant la commande 'yunohost log share {name}'",
- "log_does_exists": "Il n’y a pas de journal des opérations avec le nom '{log}', utilisez 'yunohost log list' pour voir tous les journaux d’opérations disponibles",
- "log_operation_unit_unclosed_properly": "L’opération ne s’est pas terminée correctement",
- "log_app_change_url": "Changer l’URL de l’application '{}'",
- "log_app_install": "Installer l’application '{}'",
- "log_app_remove": "Enlever l’application '{}'",
- "log_app_upgrade": "Mettre à jour l’application '{}'",
- "log_app_makedefault": "Faire de '{}' l’application par défaut",
+ "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}'",
+ "log_link_to_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en cliquant ici",
+ "log_help_to_get_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en utilisant la commande 'yunohost log share {name}'",
+ "log_does_exists": "Il n'y a pas de journal des opérations avec le nom '{log}', utilisez 'yunohost log list' pour voir tous les journaux d'opérations disponibles",
+ "log_operation_unit_unclosed_properly": "L'opération ne s'est pas terminée correctement",
+ "log_app_change_url": "Changer l'URL de l'application '{}'",
+ "log_app_install": "Installer l'application '{}'",
+ "log_app_remove": "Enlever l'application '{}'",
+ "log_app_upgrade": "Mettre à jour l'application '{}'",
+ "log_app_makedefault": "Faire de '{}' l'application par défaut",
"log_available_on_yunopaste": "Le journal est désormais disponible via {url}",
"log_backup_restore_system": "Restaurer le système depuis une archive de sauvegarde",
"log_backup_restore_app": "Restaurer '{}' depuis une sauvegarde",
@@ -296,39 +266,30 @@
"log_domain_add": "Ajouter le domaine '{}' dans la configuration du système",
"log_domain_remove": "Enlever le domaine '{}' de la configuration du système",
"log_dyndns_subscribe": "Souscrire au sous-domaine YunoHost '{}'",
- "log_dyndns_update": "Mettre à jour l’adresse IP associée à votre sous-domaine YunoHost '{}'",
- "log_letsencrypt_cert_install": "Installer le certificat Let’s Encrypt sur le domaine '{}'",
+ "log_dyndns_update": "Mettre à jour l'adresse IP associée à votre sous-domaine YunoHost '{}'",
+ "log_letsencrypt_cert_install": "Installer le certificat Let's Encrypt sur le domaine '{}'",
"log_selfsigned_cert_install": "Installer un certificat auto-signé sur le domaine '{}'",
- "log_letsencrypt_cert_renew": "Renouveler le certificat Let’s Encrypt de '{}'",
- "log_user_create": "Ajouter l’utilisateur '{}'",
- "log_user_delete": "Supprimer l’utilisateur '{}'",
- "log_user_update": "Mettre à jour les informations de l’utilisateur '{}'",
+ "log_letsencrypt_cert_renew": "Renouveler le certificat Let's Encrypt de '{}'",
+ "log_user_create": "Ajouter l'utilisateur '{}'",
+ "log_user_delete": "Supprimer l'utilisateur '{}'",
+ "log_user_update": "Mettre à jour les informations de l'utilisateur '{}'",
"log_domain_main_domain": "Faire de '{}' le domaine principal",
"log_tools_migrations_migrate_forward": "Exécuter les migrations",
"log_tools_postinstall": "Faire la post-installation de votre serveur YunoHost",
"log_tools_upgrade": "Mettre à jour les paquets du système",
"log_tools_shutdown": "Éteindre votre serveur",
"log_tools_reboot": "Redémarrer votre serveur",
- "mail_unavailable": "Cette adresse de courriel est réservée et doit être automatiquement attribuée au tout premier utilisateur",
- "migration_description_0004_php5_to_php7_pools": "Reconfigurer l'ensemble PHP pour utiliser PHP 7 au lieu de PHP 5",
- "migration_description_0005_postgresql_9p4_to_9p6": "Migration des bases de données de PostgreSQL 9.4 vers PostgreSQL 9.6",
- "migration_0005_postgresql_94_not_installed": "PostgreSQL n’a pas été installé sur votre système. Rien à faire !",
- "migration_0005_postgresql_96_not_installed": "PostgreSQL 9.4 est installé, mais pas PostgreSQL 9.6 ‽ Quelque chose de bizarre aurait pu se produire sur votre système :(…",
- "migration_0005_not_enough_space": "Laissez suffisamment d’espace disponible dans {path} pour exécuter la migration.",
- "service_description_php7.0-fpm": "Exécute des applications écrites en PHP avec NGINX",
- "users_available": "Liste des utilisateurs disponibles :",
- "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe d'administration. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase de passe) et / ou d'utiliser une variation de caractères (majuscule, minuscule, chiffres et caractères spéciaux).",
- "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et / ou une variation de caractères (majuscule, minuscule, chiffres et caractères spéciaux).",
- "migration_description_0006_sync_admin_and_root_passwords": "Synchroniser les mots de passe admin et root",
- "migration_0006_disclaimer": "YunoHost s’attend maintenant à ce que les mots de passe administrateur et racine soient synchronisés. Cette migration remplace votre mot de passe root par le mot de passe administrateur.",
- "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés au monde. Veuillez choisir quelque chose de plus unique.",
+ "mail_unavailable": "Cette adresse d'email est réservée et doit être automatiquement attribuée au tout premier utilisateur",
+ "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe d'administration. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).",
+ "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).",
+ "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus robuste.",
"password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères",
"password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules et des minuscules",
"password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux",
"password_too_simple_4": "Le mot de passe doit comporter au moins 12 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux",
- "root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost n’a pas pu le propager au mot de passe root !",
- "aborting": "Annulation.",
- "app_not_upgraded": "L’application {failed_app} n’a pas été mise à jour et par conséquence les applications suivantes n’ont pas été mises à jour : {apps}",
+ "root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost n'a pas pu le propager au mot de passe root !",
+ "aborting": "Annulation en cours.",
+ "app_not_upgraded": "L'application {failed_app} n'a pas été mise à jour et par conséquence les applications suivantes n'ont pas été mises à jour : {apps}",
"app_start_install": "Installation de {app}...",
"app_start_remove": "Suppression de {app}...",
"app_start_backup": "Collecte des fichiers devant être sauvegardés pour {app}...",
@@ -336,42 +297,30 @@
"app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}",
"ask_new_domain": "Nouveau domaine",
"ask_new_path": "Nouveau chemin",
- "backup_actually_backuping": "Création d’une archive de sauvegarde à partir des fichiers collectés...",
- "backup_mount_archive_for_restore": "Préparation de l’archive pour restauration...",
- "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n’est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l’authentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. L’installer quand même ? [{answers:s}] ",
- "confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS l’installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système … Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'",
- "confirm_app_install_thirdparty": "DANGER! Cette application ne fait pas partie du catalogue d'applications de Yunohost. L'installation d'applications tierces peut compromettre l'intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système ... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'",
+ "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés ...",
+ "backup_mount_archive_for_restore": "Préparation de l'archive pour restauration...",
+ "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais n'est pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que l'authentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. L'installer quand même ? [{answers}] ",
+ "confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers}'",
+ "confirm_app_install_thirdparty": "DANGER ! Cette application ne fait pas partie du catalogue d'applications de YunoHost. L'installation d'applications tierces peut compromettre l'intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers}'",
"dpkg_is_broken": "Vous ne pouvez pas faire ça maintenant car dpkg/apt (le gestionnaire de paquets du système) semble avoir laissé des choses non configurées. Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a'.",
- "dyndns_could_not_check_available": "Impossible de vérifier si {domain:s} est disponible chez {provider:s}.",
- "file_does_not_exist": "Le fichier dont le chemin est {path:s} n’existe pas.",
+ "dyndns_could_not_check_available": "Impossible de vérifier si {domain} est disponible chez {provider}.",
+ "file_does_not_exist": "Le fichier dont le chemin est {path} n'existe pas.",
"global_settings_setting_security_password_admin_strength": "Qualité du mot de passe administrateur",
- "global_settings_setting_security_password_user_strength": "Qualité du mot de passe de l’utilisateur",
- "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autoriser l’utilisation de la clé hôte DSA (obsolète) pour la configuration du service SSH",
- "hook_json_return_error": "Échec de la lecture au retour du script {path:s}. Erreur : {msg:s}. Contenu brut : {raw_content}",
- "migration_description_0007_ssh_conf_managed_by_yunohost_step1": "La configuration SSH sera gérée par YunoHost (étape 1, automatique)",
- "migration_description_0008_ssh_conf_managed_by_yunohost_step2": "La configuration SSH sera gérée par YunoHost (étape 2, manuelle)",
- "migration_0007_cancelled": "Impossible d’améliorer la gestion de votre configuration SSH.",
- "migration_0007_cannot_restart": "SSH ne peut pas être redémarré après avoir essayé d’annuler la migration numéro 6.",
- "migration_0008_general_disclaimer": "Pour améliorer la sécurité de votre serveur, il est recommandé de laisser YunoHost gérer la configuration SSH. Votre configuration SSH actuelle diffère de la configuration recommandée. Si vous laissez YunoHost la reconfigurer, la façon dont vous vous connectez à votre serveur via SSH changera comme suit :",
- "migration_0008_port": "- Vous devrez vous connecter en utilisant le port 22 au lieu de votre actuel port SSH personnalisé. N’hésitez pas à le reconfigurer ;",
- "migration_0008_root": "- Vous ne pourrez pas vous connecter en tant que root via SSH. Au lieu de cela, vous devrez utiliser l’utilisateur admin ;",
- "migration_0008_dsa": "- La clé DSA sera désactivée. Par conséquent, il se peut que vous ayez besoin d’invalider un avertissement effrayant de votre client SSH afin de revérifier l’empreinte de votre serveur ;",
- "migration_0008_warning": "Si vous comprenez ces avertissements et souhaitez que YunoHost écrase votre configuration actuelle, exécutez la migration. Sinon, vous pouvez également ignorer la migration, bien que cela ne soit pas recommandé.",
- "migration_0008_no_warning": "Remplacer votre configuration SSH ne devrait pas poser de problème, bien qu'il soit difficile de le promettre ! Exécutez la migration pour la remplacer. Sinon, vous pouvez également ignorer la migration, bien que cela ne soit pas recommandé.",
- "migrations_success": "Migration {number} {name} réussie !",
- "pattern_password_app": "Désolé, les mots de passe ne peuvent pas contenir les caractères suivants : {forbidden_chars}",
+ "global_settings_setting_security_password_user_strength": "Qualité du mot de passe de l'utilisateur",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autoriser l'utilisation de la clé hôte DSA (obsolète) pour la configuration du service SSH",
+ "hook_json_return_error": "Échec de la lecture au retour du script {path}. Erreur : {msg}. Contenu brut : {raw_content}",
+ "pattern_password_app": "Désolé, les mots de passe ne peuvent pas contenir les caractères suivants : {forbidden_chars}",
"root_password_replaced_by_admin_password": "Votre mot de passe root a été remplacé par votre mot de passe administrateur.",
- "service_reload_failed": "Impossible de recharger le service '{service:s}'.\n\nJournaux historisés récents de ce service : {logs:s}",
- "service_reloaded": "Le service « {service:s} » a été rechargé",
- "service_restart_failed": "Impossible de redémarrer le service '{service:s}'\n\nJournaux historisés récents de ce service : {logs:s}",
- "service_restarted": "Le service « {service:s} » a été redémarré",
- "service_reload_or_restart_failed": "Impossible de recharger ou de redémarrer le service '{service:s}'\n\nJournaux historisés récents de ce service : {logs:s}",
- "service_reloaded_or_restarted": "Le service « {service:s} » a été rechargé ou redémarré",
- "this_action_broke_dpkg": "Cette action a laissé des paquets non configurés par dpkg/apt (les gestionnaires de paquets du système) … Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a`.",
- "app_action_cannot_be_ran_because_required_services_down": "Ces services requis doivent être en cours d’exécution pour exécuter cette action : {services}. Essayez de les redémarrer pour continuer (et éventuellement rechercher pourquoi ils sont en panne).",
+ "service_reload_failed": "Impossible de recharger le service '{service}'.\n\nJournaux historisés récents de ce service : {logs}",
+ "service_reloaded": "Le service '{service}' a été rechargé",
+ "service_restart_failed": "Impossible de redémarrer le service '{service}'\n\nJournaux historisés récents de ce service : {logs}",
+ "service_restarted": "Le service '{service}' a été redémarré",
+ "service_reload_or_restart_failed": "Impossible de recharger ou de redémarrer le service '{service}'\n\nJournaux historisés récents de ce service : {logs}",
+ "service_reloaded_or_restarted": "Le service '{service}' a été rechargé ou redémarré",
+ "this_action_broke_dpkg": "Cette action a laissé des paquets non configurés par dpkg/apt (les gestionnaires de paquets du système) ... Vous pouvez essayer de résoudre ce problème en vous connectant via SSH et en exécutant `sudo apt install --fix-broken` et/ou `sudo dpkg --configure -a`.",
+ "app_action_cannot_be_ran_because_required_services_down": "Ces services requis doivent être en cours d'exécution pour exécuter cette action : {services}. Essayez de les redémarrer pour continuer (et éventuellement rechercher pourquoi ils sont en panne).",
"admin_password_too_long": "Veuillez choisir un mot de passe comportant moins de 127 caractères",
"log_regen_conf": "Régénérer les configurations du système '{}'",
- "migration_0009_not_needed": "Cette migration semble avoir déjà été jouée ? On l’ignore.",
"regenconf_file_backed_up": "Le fichier de configuration '{conf}' a été sauvegardé sous '{backup}'",
"regenconf_file_copy_failed": "Impossible de copier le nouveau fichier de configuration '{new}' vers '{conf}'",
"regenconf_file_manually_modified": "Le fichier de configuration '{conf}' a été modifié manuellement et ne sera pas mis à jour",
@@ -381,113 +330,95 @@
"regenconf_file_updated": "Le fichier de configuration '{conf}' a été mis à jour",
"regenconf_now_managed_by_yunohost": "Le fichier de configuration '{conf}' est maintenant géré par YunoHost (catégorie {category}).",
"regenconf_up_to_date": "La configuration est déjà à jour pour la catégorie '{category}'",
- "already_up_to_date": "Il n’y a rien à faire ! Tout est déjà à jour !",
- "global_settings_setting_security_nginx_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur web Nginx. Affecte les cryptogrammes (et d’autres aspects liés à la sécurité)",
- "global_settings_setting_security_ssh_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur SSH. Affecte les cryptogrammes (et d’autres aspects liés à la sécurité)",
- "global_settings_setting_security_postfix_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur Postfix. Affecte les cryptogrammes (et d’autres aspects liés à la sécurité)",
- "migration_description_0009_decouple_regenconf_from_services": "Dissocier le mécanisme « regen-conf » des services",
- "migration_description_0010_migrate_to_apps_json": "Supprimer les catalogues d’applications obsolètes afin d’utiliser la nouvelle liste unifiée 'apps.json' à la place (les anciens catalogues seront remplacés durant la migration 13)",
- "regenconf_file_kept_back": "Le fichier de configuration '{conf}' devait être supprimé par « regen-conf » (catégorie {category}) mais a été conservé.",
+ "already_up_to_date": "Il n'y a rien à faire. Tout est déjà à jour.",
+ "global_settings_setting_security_nginx_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur web Nginx. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)",
+ "global_settings_setting_security_ssh_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur SSH. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)",
+ "global_settings_setting_security_postfix_compatibility": "Compatibilité versus compromis sécuritaire pour le serveur Postfix. Affecte les cryptogrammes (et d'autres aspects liés à la sécurité)",
+ "regenconf_file_kept_back": "Le fichier de configuration '{conf}' devait être supprimé par 'regen-conf' (catégorie {category}) mais a été conservé.",
"regenconf_updated": "La configuration a été mise à jour pour '{category}'",
"regenconf_would_be_updated": "La configuration aurait dû être mise à jour pour la catégorie '{category}'",
- "regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}'…",
+ "regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}'...",
"regenconf_failed": "Impossible de régénérer la configuration pour la ou les catégorie(s) : '{categories}'",
"regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}'...",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' est obsolète ! Veuillez plutôt utiliser 'yunohost tools regen-conf' à la place.",
"tools_upgrade_at_least_one": "Veuillez spécifier '--apps' ou '--system'",
"tools_upgrade_cant_both": "Impossible de mettre à niveau le système et les applications en même temps",
- "tools_upgrade_cant_hold_critical_packages": "Impossibilité d'ajouter le drapeau 'hold' pour les paquets critiques…",
- "tools_upgrade_regular_packages": "Mise à jour des paquets du système (non liés a YunoHost)…",
+ "tools_upgrade_cant_hold_critical_packages": "Impossibilité d'ajouter le drapeau 'hold' pour les paquets critiques...",
+ "tools_upgrade_regular_packages": "Mise à jour des paquets du système (non liés a YunoHost)...",
"tools_upgrade_regular_packages_failed": "Impossible de mettre à jour les paquets suivants : {packages_list}",
- "tools_upgrade_special_packages": "Mise à jour des paquets 'spécifiques' (liés a YunoHost)…",
+ "tools_upgrade_special_packages": "Mise à jour des paquets 'spécifiques' (liés a YunoHost)...",
"tools_upgrade_special_packages_completed": "La mise à jour des paquets de YunoHost est finie !\nPressez [Entrée] pour revenir à la ligne de commande",
"dpkg_lock_not_available": "Cette commande ne peut pas être exécutée pour le moment car un autre programme semble utiliser le verrou de dpkg (le gestionnaire de package système)",
- "tools_upgrade_cant_unhold_critical_packages": "Impossible d'enlever le drapeau 'hold' pour les paquets critiques…",
- "tools_upgrade_special_packages_explanation": "La mise à niveau spécifique à YunoHost se poursuivra en arrière-plan. Veuillez ne pas lancer d'autres actions sur votre serveur pendant les 10 prochaines minutes (selon la vitesse du matériel). Après cela, vous devrez peut-être vous reconnecter à l'administrateur Web. Le journal de mise à niveau sera disponible dans Outils → Journal (dans le webadmin) ou en utilisant la « liste des journaux yunohost » (à partir de la ligne de commande).",
+ "tools_upgrade_cant_unhold_critical_packages": "Impossible d'enlever le drapeau 'hold' pour les paquets critiques...",
+ "tools_upgrade_special_packages_explanation": "La mise à niveau spécifique à YunoHost se poursuivra en arrière-plan. Veuillez ne pas lancer d'autres actions sur votre serveur pendant les 10 prochaines minutes (selon la vitesse du matériel). Après cela, vous devrez peut-être vous reconnecter à la webadmin. Le journal de mise à niveau sera disponible dans Outils → Journal (dans le webadmin) ou en utilisant 'yunohost log list' (à partir de la ligne de commande).",
"update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}",
"update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}",
- "backup_permission": "Permission de sauvegarde pour {app:s}",
+ "backup_permission": "Permission de sauvegarde pour {app}",
"group_created": "Le groupe '{group}' a été créé",
"group_deleted": "Suppression du groupe '{group}'",
- "group_unknown": "Le groupe {group:s} est inconnu",
+ "group_unknown": "Le groupe {group} est inconnu",
"group_updated": "Le groupe '{group}' a été mis à jour",
"group_update_failed": "La mise à jour du groupe '{group}' a échoué : {error}",
"group_creation_failed": "Échec de la création du groupe '{group}' : {error}",
"group_deletion_failed": "Échec de la suppression du groupe '{group}' : {error}",
"log_user_group_delete": "Supprimer le groupe '{}'",
"log_user_group_update": "Mettre à jour '{}' pour le groupe",
- "mailbox_disabled": "La boîte aux lettres est désactivée pour l’utilisateur {user:s}",
+ "mailbox_disabled": "La boîte aux lettres est désactivée pour l'utilisateur {user}",
"app_action_broke_system": "Cette action semble avoir cassé des services importants : {services}",
"apps_already_up_to_date": "Toutes les applications sont déjà à jour",
- "migration_0011_create_group": "Création d'un groupe pour chaque utilisateur…",
- "migration_0011_done": "Migration terminée. Vous êtes maintenant en mesure de gérer des groupes d’utilisateurs.",
"migrations_must_provide_explicit_targets": "Vous devez fournir des cibles explicites lorsque vous utilisez '--skip' ou '--force-rerun'",
- "migrations_no_such_migration": "Il n’y a pas de migration appelée '{id}'",
+ "migrations_no_such_migration": "Il n'y a pas de migration appelée '{id}'",
"migrations_pending_cant_rerun": "Ces migrations étant toujours en attente, vous ne pouvez pas les exécuter à nouveau : {ids}",
- "migration_description_0012_postgresql_password_to_md5_authentication": "Forcer l’authentification PostgreSQL à utiliser MD5 pour les connexions locales",
"migrations_exclusive_options": "'auto', '--skip' et '--force-rerun' sont des options mutuellement exclusives.",
"migrations_not_pending_cant_skip": "Ces migrations ne sont pas en attente et ne peuvent donc pas être ignorées : {ids}",
- "migration_0011_can_not_backup_before_migration": "La sauvegarde du système n’a pas pu être terminée avant l’échec de la migration. Erreur : {error:s}",
- "migration_0011_migrate_permission": "Migration des autorisations des paramètres des applications vers LDAP...",
- "migration_0011_migration_failed_trying_to_rollback": "La migration a échoué… Tentative de restauration du système.",
- "migration_0011_rollback_success": "Système restauré.",
- "migration_0011_update_LDAP_database": "Mise à jour de la base de données LDAP...",
- "migration_0011_backup_before_migration": "Création d’une sauvegarde des paramètres de la base de données LDAP et des applications avant la migration.",
- "permission_not_found": "Permission '{permission:s}' introuvable",
+ "permission_not_found": "Permission '{permission}' introuvable",
"permission_update_failed": "Impossible de mettre à jour la permission '{permission}' : {error}",
- "permission_updated": "Permission '{permission:s}' mise à jour",
- "permission_update_nothing_to_do": "Aucune permission à mettre à jour",
- "dyndns_provider_unreachable": "Impossible d’atteindre le fournisseur DynDNS {provider} : votre YunoHost n’est pas correctement connecté à Internet ou le serveur Dynette est en panne.",
- "migration_0011_update_LDAP_schema": "Mise à jour du schéma LDAP...",
+ "permission_updated": "Permission '{permission}' mise à jour",
+ "dyndns_provider_unreachable": "Impossible d'atteindre le fournisseur DynDNS {provider} : votre YunoHost n'est pas correctement connecté à Internet ou le serveur Dynette est en panne.",
"migrations_already_ran": "Ces migrations sont déjà effectuées : {ids}",
"migrations_dependencies_not_satisfied": "Exécutez ces migrations : '{dependencies_id}', avant migration {id}.",
"migrations_failed_to_load_migration": "Impossible de charger la migration {id} : {error}",
"migrations_running_forward": "Exécution de la migration {id}...",
"migrations_success_forward": "Migration {id} terminée",
- "operation_interrupted": "L’opération a été interrompue manuellement ?",
- "permission_already_exist": "L’autorisation '{permission}' existe déjà",
- "permission_created": "Permission '{permission:s}' créée",
- "permission_creation_failed": "Impossible de créer l’autorisation '{permission}' : {error}",
- "permission_deleted": "Permission '{permission:s}' supprimée",
+ "operation_interrupted": "L'opération a-t-elle été interrompue manuellement ?",
+ "permission_already_exist": "L'autorisation '{permission}' existe déjà",
+ "permission_created": "Permission '{permission}' créée",
+ "permission_creation_failed": "Impossible de créer l'autorisation '{permission}' : {error}",
+ "permission_deleted": "Permission '{permission}' supprimée",
"permission_deletion_failed": "Impossible de supprimer la permission '{permission}' : {error}",
- "migration_description_0011_setup_group_permission": "Initialiser les groupes d’utilisateurs et autorisations pour les applications et les services",
- "migration_0011_LDAP_update_failed": "Impossible de mettre à jour LDAP. Erreur : {error:s}",
"group_already_exist": "Le groupe {group} existe déjà",
"group_already_exist_on_system": "Le groupe {group} existe déjà dans les groupes système",
"group_cannot_be_deleted": "Le groupe {group} ne peut pas être supprimé manuellement.",
- "group_user_already_in_group": "L’utilisateur {user} est déjà dans le groupe {group}",
- "group_user_not_in_group": "L’utilisateur {user} n’est pas dans le groupe {group}",
+ "group_user_already_in_group": "L'utilisateur {user} est déjà dans le groupe {group}",
+ "group_user_not_in_group": "L'utilisateur {user} n'est pas dans le groupe {group}",
"log_permission_create": "Créer permission '{}'",
"log_permission_delete": "Supprimer permission '{}'",
"log_user_group_create": "Créer le groupe '{}'",
"log_user_permission_update": "Mise à jour des accès pour la permission '{}'",
"log_user_permission_reset": "Réinitialiser la permission '{}'",
- "migration_0011_failed_to_remove_stale_object": "Impossible de supprimer un objet périmé {dn} : {error}",
- "permission_already_allowed": "Le groupe '{group}' a déjà l’autorisation '{permission}' activée",
- "permission_already_disallowed": "Le groupe '{group}' a déjà l’autorisation '{permission}' désactivé",
- "permission_cannot_remove_main": "Supprimer une autorisation principale n’est pas autorisé",
- "user_already_exists": "L’utilisateur '{user}' existe déjà",
- "app_full_domain_unavailable": "Désolé, cette application doit être installée sur un domaine qui lui est propre, mais d’autres applications sont déjà installées sur le domaine '{domain}'. Vous pouvez utiliser un sous-domaine dédié à cette application à la place.",
- "group_cannot_edit_all_users": "Le groupe 'all_users' ne peut pas être édité manuellement. C’est un groupe spécial destiné à contenir tous les utilisateurs enregistrés dans YunoHost",
- "group_cannot_edit_visitors": "Le groupe 'visiteurs' ne peut pas être édité manuellement. C’est un groupe spécial représentant les visiteurs anonymes",
- "group_cannot_edit_primary_group": "Le groupe '{group}' ne peut pas être édité manuellement. C’est le groupe principal destiné à ne contenir qu’un utilisateur spécifique.",
- "log_permission_url": "Mise à jour de l’URL associée à l’autorisation '{}'",
- "migration_0011_slapd_config_will_be_overwritten": "Il semble que vous ayez modifié manuellement la configuration de slapd. Pour cette migration critique, YunoHost doit forcer la mise à jour de la configuration de slapd. Les fichiers originaux seront sauvegardés dans {conf_backup_folder}.",
- "permission_already_up_to_date": "L’autorisation n’a pas été mise à jour car les demandes d’ajout/suppression correspondent déjà à l’état actuel.",
- "permission_currently_allowed_for_all_users": "Cette autorisation est actuellement accordée à tous les utilisateurs en plus des autres groupes. Vous voudrez probablement soit supprimer l’autorisation 'all_users', soit supprimer les autres groupes auxquels il est actuellement autorisé.",
- "app_install_failed": "Impossible d’installer {app} : {error}",
- "app_install_script_failed": "Une erreur est survenue dans le script d’installation de l’application",
- "permission_require_account": "Permission {permission} n’a de sens que pour les utilisateurs ayant un compte et ne peut donc pas être activé pour les visiteurs.",
- "app_remove_after_failed_install": "Supprimer l’application après l’échec de l’installation...",
- "diagnosis_display_tip_web": "Vous pouvez aller à la section Diagnostic (dans l’écran d’accueil) pour voir les problèmes rencontrés.",
- "diagnosis_cant_run_because_of_dep": "Impossible d’exécuter le diagnostic pour {category} alors qu’il existe des problèmes importants liés à {dep}.",
+ "permission_already_allowed": "Le groupe '{group}' a déjà l'autorisation '{permission}' activée",
+ "permission_already_disallowed": "Le groupe '{group}' a déjà l'autorisation '{permission}' désactivé",
+ "permission_cannot_remove_main": "Supprimer une autorisation principale n'est pas autorisé",
+ "user_already_exists": "L'utilisateur '{user}' existe déjà",
+ "app_full_domain_unavailable": "Désolé, cette application doit être installée sur un domaine qui lui est propre, mais d'autres applications sont déjà installées sur le domaine '{domain}'. Vous pouvez utiliser un sous-domaine dédié à cette application à la place.",
+ "group_cannot_edit_all_users": "Le groupe 'all_users' ne peut pas être édité manuellement. C'est un groupe spécial destiné à contenir tous les utilisateurs enregistrés dans YunoHost",
+ "group_cannot_edit_visitors": "Le groupe 'visiteurs' ne peut pas être édité manuellement. C'est un groupe spécial représentant les visiteurs anonymes",
+ "group_cannot_edit_primary_group": "Le groupe '{group}' ne peut pas être édité manuellement. C'est le groupe principal destiné à ne contenir qu'un utilisateur spécifique.",
+ "log_permission_url": "Mise à jour de l'URL associée à l'autorisation '{}'",
+ "permission_already_up_to_date": "L'autorisation n'a pas été mise à jour car les demandes d'ajout/suppression correspondent déjà à l'état actuel.",
+ "permission_currently_allowed_for_all_users": "Cette autorisation est actuellement accordée à tous les utilisateurs en plus des autres groupes. Vous voudrez probablement soit supprimer l'autorisation 'all_users', soit supprimer les autres groupes auxquels il est actuellement autorisé.",
+ "app_install_failed": "Impossible d'installer {app} : {error}",
+ "app_install_script_failed": "Une erreur est survenue dans le script d'installation de l'application",
+ "permission_require_account": "Permission {permission} n'a de sens que pour les utilisateurs ayant un compte et ne peut donc pas être activé pour les visiteurs.",
+ "app_remove_after_failed_install": "Supprimer l'application après l'échec de l'installation...",
+ "diagnosis_cant_run_because_of_dep": "Impossible d'exécuter le diagnostic pour {category} alors qu'il existe des problèmes importants liés à {dep}.",
"diagnosis_found_errors": "Trouvé {errors} problème(s) significatif(s) lié(s) à {category} !",
"diagnosis_found_errors_and_warnings": "Trouvé {errors} problème(s) significatif(s) (et {warnings} (avertissement(s)) en relation avec {category} !",
"diagnosis_ip_not_connected_at_all": "Le serveur ne semble pas du tout connecté à Internet !?",
"diagnosis_ip_weird_resolvconf": "La résolution DNS semble fonctionner, mais il semble que vous utilisez un /etc/resolv.conf
personnalisé.",
"diagnosis_ip_weird_resolvconf_details": "Le fichier /etc/resolv.conf
doit être un lien symbolique vers /etc/resolvconf/run/resolv.conf
lui-même pointant vers 127.0.0.1
(dnsmasq). Si vous souhaitez configurer manuellement les résolveurs DNS, veuillez modifier /etc/resolv.dnsmasq.conf
.",
- "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS
Type : {type}
Nom : {name}
Valeur: {value}
",
- "diagnosis_diskusage_ok": "L’espace de stockage {mountpoint}
(sur le périphérique {device}
) a encore {free} ({free_percent}%) espace restant (sur {total}) !",
+ "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS
Type : {type}
Nom : {name}
Valeur : {value}
",
+ "diagnosis_diskusage_ok": "L'espace de stockage {mountpoint}
(sur le périphérique {device}
) a encore {free} ({free_percent}%) espace restant (sur {total}) !",
"diagnosis_ram_ok": "Le système dispose encore de {available} ({available_percent}%) de RAM sur {total}.",
"diagnosis_regenconf_allgood": "Tous les fichiers de configuration sont conformes à la configuration recommandée !",
"diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la vulnérabilité de sécurité critique de Meltdown",
@@ -495,41 +426,38 @@
"diagnosis_basesystem_kernel": "Le serveur utilise le noyau Linux {kernel_version}",
"diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})",
"diagnosis_basesystem_ynh_main_version": "Le serveur utilise YunoHost {main_version} ({repo})",
- "diagnosis_basesystem_ynh_inconsistent_versions": "Vous exécutez des versions incohérentes des packages YunoHost ... très probablement en raison d’une mise à niveau échouée ou partielle.",
- "diagnosis_display_tip_cli": "Vous pouvez exécuter 'yunohost diagnosis show --issues' pour afficher les problèmes détectés.",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Vous exécutez des versions incohérentes des packages YunoHost ... très probablement en raison d'une mise à niveau échouée ou partielle.",
"diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}': {error}",
- "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment!)",
+ "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)",
"diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))",
"diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.",
- "diagnosis_everything_ok": "Tout semble bien pour {category} !",
+ "diagnosis_everything_ok": "Tout semble OK pour {category} !",
"diagnosis_failed": "Échec de la récupération du résultat du diagnostic pour la catégorie '{category}' : {error}",
- "diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4!",
- "diagnosis_ip_no_ipv4": "Le serveur ne dispose pas d’une adresse IPv4.",
- "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6!",
- "diagnosis_ip_no_ipv6": "Le serveur ne dispose pas d’une adresse IPv6.",
+ "diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !",
+ "diagnosis_ip_no_ipv4": "Le serveur ne dispose pas d'une adresse IPv4.",
+ "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6 !",
+ "diagnosis_ip_no_ipv6": "Le serveur ne dispose pas d'une adresse IPv6.",
"diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !",
- "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble interrompue pour une raison quelconque … Un pare-feu bloque-t-il les requêtes DNS ?",
+ "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble interrompue pour une raison quelconque ... Un pare-feu bloque-t-il les requêtes DNS ?",
"diagnosis_ip_broken_resolvconf": "La résolution du nom de domaine semble être cassée sur votre serveur, ce qui semble lié au fait que /etc/resolv.conf
ne pointe pas vers 127.0.0.1
.",
"diagnosis_dns_good_conf": "Les enregistrements DNS sont correctement configurés pour le domaine {domain} (catégorie {category})",
"diagnosis_dns_bad_conf": "Certains enregistrements DNS sont manquants ou incorrects pour le domaine {domain} (catégorie {category})",
"diagnosis_dns_discrepancy": "Cet enregistrement DNS ne semble pas correspondre à la configuration recommandée :
Type : {type}
Nom : {name}
La valeur actuelle est : {current}
La valeur attendue est : {value}
",
"diagnosis_services_bad_status": "Le service {service} est {status} :-(",
- "diagnosis_diskusage_verylow": "L'espace de stockage {mountpoint}
(sur l’appareil {device}
) ne dispose que de {free} ({free_percent}%) espace restant (sur {total}). Vous devriez vraiment envisager de nettoyer de l’espace !",
+ "diagnosis_diskusage_verylow": "L'espace de stockage {mountpoint}
(sur l'appareil {device}
) ne dispose que de {free} ({free_percent}%) espace restant (sur {total}). Vous devriez vraiment envisager de nettoyer de l'espace !",
"diagnosis_diskusage_low": "L'espace de stockage {mountpoint}
(sur l'appareil {device}
) ne dispose que de {free} ({free_percent}%) espace restant (sur {total}). Faites attention.",
"diagnosis_ram_verylow": "Le système ne dispose plus que de {available} ({available_percent}%)! (sur {total})",
- "diagnosis_ram_low": "Le système n’a plus de {available} ({available_percent}%) RAM sur {total}. Faites attention.",
- "diagnosis_swap_none": "Le système n’a aucun espace de swap. Vous devriez envisager d’ajouter au moins {recommended} de swap pour éviter les situations où le système manque de mémoire.",
- "diagnosis_swap_notsomuch": "Le système ne dispose que de {total} de swap. Vous devez envisager d’avoir au moins {recommended} pour éviter les situations où le système manque de mémoire.",
+ "diagnosis_ram_low": "Le système n'a plus de {available} ({available_percent}%) RAM sur {total}. Faites attention.",
+ "diagnosis_swap_none": "Le système n'a aucun espace de swap. Vous devriez envisager d'ajouter au moins {recommended} de swap pour éviter les situations où le système manque de mémoire.",
+ "diagnosis_swap_notsomuch": "Le système ne dispose que de {total} de swap. Vous devez envisager d'avoir au moins {recommended} pour éviter les situations où le système manque de mémoire.",
"diagnosis_swap_ok": "Le système dispose de {total} de swap !",
"diagnosis_regenconf_manually_modified": "Le fichier de configuration {file}
semble avoir été modifié manuellement.",
- "diagnosis_regenconf_manually_modified_debian": "Le fichier de configuration {file} a été modifié manuellement par rapport à celui par défaut de Debian.",
- "diagnosis_regenconf_manually_modified_details": "C’est probablement OK si vous savez ce que vous faites ! YunoHost cessera de mettre à jour ce fichier automatiquement ... Mais attention, les mises à jour de YunoHost pourraient contenir d’importantes modifications recommandées. Si vous le souhaitez, vous pouvez inspecter les différences avec yunohost tools regen-conf {category} --dry-run --with-diff et forcer la réinitialisation à la configuration recommandée avec yunohost tools regen-conf {category} --force ",
- "diagnosis_regenconf_manually_modified_debian_details": "Cela peut probablement être OK, mais il faut garder un œil dessus …",
- "apps_catalog_init_success": "Système de catalogue d’applications initialisé !",
+ "diagnosis_regenconf_manually_modified_details": "C'est probablement OK si vous savez ce que vous faites ! YunoHost cessera de mettre à jour ce fichier automatiquement ... Mais attention, les mises à jour de YunoHost pourraient contenir d'importantes modifications recommandées. Si vous le souhaitez, vous pouvez inspecter les différences avec yunohost tools regen-conf {category} --dry-run --with-diff et forcer la réinitialisation à la configuration recommandée avec yunohost tools regen-conf {category} --force ",
+ "apps_catalog_init_success": "Système de catalogue d'applications initialisé !",
"apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog} : {error}",
- "diagnosis_mail_outgoing_port_25_blocked": "Le port sortant 25 semble être bloqué. Vous devriez essayer de le débloquer dans le panneau de configuration de votre fournisseur de services Internet (ou hébergeur). En attendant, le serveur ne pourra pas envoyer des courriels à d’autres serveurs.",
- "domain_cannot_remove_main_add_new_one": "Vous ne pouvez pas supprimer '{domain:s}' car il s’agit du domaine principal et de votre seul domaine. Vous devez d’abord ajouter un autre domaine à l’aide de 'yunohost domain add ', puis définir comme domaine principal à l’aide de 'yunohost domain main-domain -n ' et vous pouvez ensuite supprimer le domaine '{domain:s}' à l’aide de 'yunohost domain remove {domain:s}'.'",
- "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à niveau votre système et redémarrer pour charger le nouveau noyau Linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus d’informations.",
+ "diagnosis_mail_outgoing_port_25_blocked": "Le port sortant 25 semble être bloqué. Vous devriez essayer de le débloquer dans le panneau de configuration de votre fournisseur de services Internet (ou hébergeur). En attendant, le serveur ne pourra pas envoyer des emails à d'autres serveurs.",
+ "domain_cannot_remove_main_add_new_one": "Vous ne pouvez pas supprimer '{domain}' car il s'agit du domaine principal et de votre seul domaine. Vous devez d'abord ajouter un autre domaine à l'aide de 'yunohost domain add ', puis définir comme domaine principal à l'aide de 'yunohost domain main-domain -n ' et vous pouvez ensuite supprimer le domaine '{domain}' à l'aide de 'yunohost domain remove {domain}'.'",
+ "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à niveau votre système et redémarrer pour charger le nouveau noyau Linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus d'informations.",
"diagnosis_description_basesystem": "Système de base",
"diagnosis_description_ip": "Connectivité Internet",
"diagnosis_description_dnsrecords": "Enregistrements DNS",
@@ -537,120 +465,112 @@
"diagnosis_description_systemresources": "Ressources système",
"diagnosis_description_ports": "Exposition des ports",
"diagnosis_description_regenconf": "Configurations système",
- "diagnosis_description_security": "Contrôles de sécurité",
"diagnosis_ports_could_not_diagnose": "Impossible de diagnostiquer si les ports sont accessibles de l'extérieur.",
"diagnosis_ports_could_not_diagnose_details": "Erreur : {error}",
- "apps_catalog_updating": "Mise à jour du catalogue d’applications…",
+ "apps_catalog_updating": "Mise à jour du catalogue d'applications...",
"apps_catalog_obsolete_cache": "Le cache du catalogue d'applications est vide ou obsolète.",
"apps_catalog_update_success": "Le catalogue des applications a été mis à jour !",
- "diagnosis_mail_ougoing_port_25_ok": "Le port sortant 25 n’est pas bloqué et le courrier électronique peut être envoyé à d’autres serveurs.",
- "diagnosis_description_mail": "E-mail",
- "diagnosis_ports_unreachable": "Le port {port} n’est pas accessible de l’extérieur.",
- "diagnosis_ports_ok": "Le port {port} est accessible de l’extérieur.",
- "diagnosis_http_could_not_diagnose": "Impossible de diagnostiquer si le domaine est accessible de l’extérieur.",
+ "diagnosis_description_mail": "Email",
+ "diagnosis_ports_unreachable": "Le port {port} n'est pas accessible de l'extérieur.",
+ "diagnosis_ports_ok": "Le port {port} est accessible de l'extérieur.",
+ "diagnosis_http_could_not_diagnose": "Impossible de diagnostiquer si le domaine est accessible de l'extérieur.",
"diagnosis_http_could_not_diagnose_details": "Erreur : {error}",
- "diagnosis_http_ok": "Le domaine {domain} est accessible en HTTP depuis l’extérieur.",
- "diagnosis_http_unreachable": "Le domaine {domain} est inaccessible en HTTP depuis l’extérieur.",
+ "diagnosis_http_ok": "Le domaine {domain} est accessible en HTTP depuis l'extérieur.",
+ "diagnosis_http_unreachable": "Le domaine {domain} est inaccessible en HTTP depuis l'extérieur.",
"diagnosis_unknown_categories": "Les catégories suivantes sont inconnues : {categories}",
- "migration_description_0013_futureproof_apps_catalog_system": "Migrer vers le nouveau système de catalogue d’applications à l’épreuve du temps",
- "app_upgrade_script_failed": "Une erreur s’est produite durant l’exécution du script de mise à niveau de l’application",
- "migration_description_0014_remove_app_status_json": "Supprimer les anciens fichiers d’application status.json",
+ "app_upgrade_script_failed": "Une erreur s'est produite durant l'exécution du script de mise à niveau de l'application",
"diagnosis_services_running": "Le service {service} est en cours de fonctionnement !",
"diagnosis_services_conf_broken": "La configuration est cassée pour le service {service} !",
"diagnosis_ports_needed_by": "Rendre ce port accessible est nécessaire pour les fonctionnalités de type {category} (service {service})",
"diagnosis_ports_forwarding_tip": "Pour résoudre ce problème, vous devez probablement configurer la redirection de port sur votre routeur Internet comme décrit dans https://yunohost.org/isp_box_config",
- "diagnosis_http_connection_error": "Erreur de connexion : impossible de se connecter au domaine demandé, il est probablement injoignable.",
- "diagnosis_no_cache": "Pas encore de cache de diagnostique pour la catégorie « {category} »",
- "yunohost_postinstall_end_tip": "La post-installation terminée! Pour finaliser votre configuration, il est recommandé de :\n - ajouter un premier utilisateur depuis la section \"Utilisateurs\" de l’interface web (ou \"yunohost user create \" en ligne de commande) ;\n - diagnostiquer les potentiels problèmes dans la section \"Diagnostic\" de l'interface web (ou \"yunohost diagnosis run\" en ligne de commande) ;\n - lire les parties \"Finalisation de votre configuration\" et \"Découverte de YunoHost\" dans le guide de l’administrateur: https://yunohost.org/admindoc.",
+ "diagnosis_http_connection_error": "Erreur de connexion : impossible de se connecter au domaine demandé, il est probablement injoignable.",
+ "diagnosis_no_cache": "Pas encore de cache de diagnostique pour la catégorie '{category}'",
+ "yunohost_postinstall_end_tip": "La post-installation terminée ! Pour finaliser votre configuration, il est recommandé de :\n- ajouter un premier utilisateur depuis la section \"Utilisateurs\" de l'interface web (ou 'yunohost user create ' en ligne de commande) ;\n- diagnostiquer les potentiels problèmes dans la section \"Diagnostic\" de l'interface web (ou 'yunohost diagnosis run' en ligne de commande) ;\n- lire les parties 'Finalisation de votre configuration' et 'Découverte de YunoHost' dans le guide de l'administrateur : https://yunohost.org/admindoc.",
"diagnosis_services_bad_status_tip": "Vous pouvez essayer de redémarrer le service, et si cela ne fonctionne pas, consultez les journaux de service dans le webadmin (à partir de la ligne de commande, vous pouvez le faire avec yunohost service restart {service} et yunohost service log {service} ).",
- "diagnosis_http_bad_status_code": "Le système de diagnostique n’a pas réussi à contacter votre serveur. Il se peut qu’une autre machine réponde à la place de votre serveur. Vérifiez que le port 80 est correctement redirigé, que votre configuration Nginx est à jour et qu’un reverse-proxy n’interfère pas.",
- "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur de l’extérieur. Il semble être inaccessible. Vérifiez que vous transférez correctement le port 80, que Nginx est en cours d’exécution et qu’un pare-feu n’interfère pas.",
+ "diagnosis_http_bad_status_code": "Le système de diagnostique n'a pas réussi à contacter votre serveur. Il se peut qu'une autre machine réponde à la place de votre serveur. Vérifiez que le port 80 est correctement redirigé, que votre configuration Nginx est à jour et qu'un reverse-proxy n'interfère pas.",
+ "diagnosis_http_timeout": "Expiration du délai en essayant de contacter votre serveur de l'extérieur. Il semble être inaccessible. Vérifiez que vous transférez correctement le port 80, que Nginx est en cours d'exécution et qu'un pare-feu n'interfère pas.",
"global_settings_setting_pop3_enabled": "Activer le protocole POP3 pour le serveur de messagerie",
- "log_app_action_run": "Lancer l’action de l’application '{}'",
- "log_app_config_show_panel": "Montrer le panneau de configuration de l’application '{}'",
- "log_app_config_apply": "Appliquer la configuration à l’application '{}'",
- "diagnosis_never_ran_yet": "Il apparaît que le serveur a été installé récemment et qu’il n’y a pas encore eu de diagnostic. Vous devriez en lancer un depuis le webmin ou en utilisant 'yunohost diagnosis run' depuis la ligne de commande.",
+ "log_app_action_run": "Lancer l'action de l'application '{}'",
+ "diagnosis_never_ran_yet": "Il apparaît que le serveur a été installé récemment et qu'il n'y a pas encore eu de diagnostic. Vous devriez en lancer un depuis la webadmin ou en utilisant 'yunohost diagnosis run' depuis la ligne de commande.",
"diagnosis_description_web": "Web",
- "diagnosis_basesystem_hardware_board": "Le modèle de carte du serveur est {model}",
- "diagnosis_basesystem_hardware": "L’architecture du serveur est {virt} {arch}",
+ "diagnosis_basesystem_hardware": "L'architecture du serveur est {virt} {arch}",
"group_already_exist_on_system_but_removing_it": "Le groupe {group} est déjà présent dans les groupes du système, mais YunoHost va le supprimer...",
- "certmanager_warning_subdomain_dns_record": "Le sous-domaine '{subdomain:s}' ne résout pas vers la même adresse IP que '{domain:s}'. Certaines fonctionnalités seront indisponibles tant que vous n’aurez pas corrigé cela et regénéré le certificat.",
- "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité d’upload XMPP intégrée dans YunoHost.",
- "diagnosis_mail_outgoing_port_25_ok": "Le serveur de messagerie SMTP peut envoyer des courriels (le port sortant 25 n'est pas bloqué).",
- "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devez d’abord essayer de débloquer le port sortant 25 dans votre interface de routeur Internet ou votre interface d’hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).",
+ "certmanager_warning_subdomain_dns_record": "Le sous-domaine '{subdomain}' ne résout pas vers la même adresse IP que '{domain}'. Certaines fonctionnalités seront indisponibles tant que vous n'aurez pas corrigé cela et regénéré le certificat.",
+ "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité d'upload XMPP intégrée dans YunoHost.",
+ "diagnosis_mail_outgoing_port_25_ok": "Le serveur de messagerie SMTP peut envoyer des emails (le port sortant 25 n'est pas bloqué).",
+ "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devez d'abord essayer de débloquer le port sortant 25 dans votre interface de routeur Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).",
"diagnosis_mail_ehlo_bad_answer": "Un service non SMTP a répondu sur le port 25 en IPv{ipversion}",
"diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond au lieu de votre serveur.",
- "diagnosis_mail_ehlo_wrong": "Un autre serveur de messagerie SMTP répond sur IPv{ipversion}. Votre serveur ne sera probablement pas en mesure de recevoir des courriel.",
- "diagnosis_mail_ehlo_could_not_diagnose": "Impossible de diagnostiquer si le serveur de messagerie postfix est accessible de l’extérieur en IPv{ipversion}.",
+ "diagnosis_mail_ehlo_wrong": "Un autre serveur de messagerie SMTP répond sur IPv{ipversion}. Votre serveur ne sera probablement pas en mesure de recevoir des email.",
+ "diagnosis_mail_ehlo_could_not_diagnose": "Impossible de diagnostiquer si le serveur de messagerie postfix est accessible de l'extérieur en IPv{ipversion}.",
"diagnosis_mail_ehlo_could_not_diagnose_details": "Erreur : {error}",
- "diagnosis_mail_fcrdns_dns_missing": "Aucun DNS inverse n’est défini pour IPv{ipversion}. Certains e-mails seront peut-être refusés ou considérés comme des spam.",
+ "diagnosis_mail_fcrdns_dns_missing": "Aucun DNS inverse n'est défini pour IPv{ipversion}. Certains emails seront peut-être refusés ou considérés comme des spam.",
"diagnosis_mail_fcrdns_ok": "Votre DNS inverse est correctement configuré !",
- "diagnosis_mail_fcrdns_nok_details": "Vous devez d’abord essayer de configurer le DNS inverse avec {ehlo_domain}
dans votre interface de routeur Internet ou votre interface d’hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).",
- "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Le DNS inverse n'est pas correctement configuré en IPv{ipversion}. Certains e-mails seront peut-être refusés ou considérés comme des spam.",
+ "diagnosis_mail_fcrdns_nok_details": "Vous devez d'abord essayer de configurer le DNS inverse avec {ehlo_domain}
dans votre interface de routeur Internet ou votre interface d'hébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Le DNS inverse n'est pas correctement configuré en IPv{ipversion}. Certains emails seront peut-être refusés ou considérés comme des spam.",
"diagnosis_mail_blacklist_ok": "Les adresses IP et les domaines utilisés par ce serveur ne semblent pas être sur liste noire",
"diagnosis_mail_blacklist_reason": "La raison de la liste noire est : {reason}",
- "diagnosis_mail_blacklist_website": "Après avoir identifié la raison pour laquelle vous êtes répertorié et l'avoir corrigé, n’hésitez pas à demander le retrait de votre IP ou domaine sur {blacklist_website}",
- "diagnosis_mail_queue_ok": "{nb_pending} e-mails en attente dans les files d'attente de messagerie",
+ "diagnosis_mail_blacklist_website": "Après avoir identifié la raison pour laquelle vous êtes répertorié et l'avoir corrigé, n'hésitez pas à demander le retrait de votre IP ou domaine sur {blacklist_website}",
+ "diagnosis_mail_queue_ok": "{nb_pending} emails en attente dans les files d'attente de messagerie",
"diagnosis_mail_queue_unavailable_details": "Erreur : {error}",
- "diagnosis_mail_queue_too_big": "Trop d’e-mails en attente dans la file d'attente ({nb_pending} e-mails)",
- "global_settings_setting_smtp_allow_ipv6": "Autoriser l'utilisation d’IPv6 pour recevoir et envoyer du courrier",
- "diagnosis_security_all_good": "Aucune vulnérabilité de sécurité critique n’a été trouvée.",
- "diagnosis_display_tip": "Pour voir les problèmes détectés, vous pouvez accéder à la section Diagnostic du webadmin ou exécuter « yunohost diagnosis show --issues » à partir de la ligne de commande.",
+ "diagnosis_mail_queue_too_big": "Trop d'emails en attente dans la file d'attente ({nb_pending} emails)",
+ "global_settings_setting_smtp_allow_ipv6": "Autoriser l'utilisation d'IPv6 pour recevoir et envoyer du courrier",
+ "diagnosis_display_tip": "Pour voir les problèmes détectés, vous pouvez accéder à la section Diagnostic du webadmin ou exécuter 'yunohost diagnosis show --issues --human-readable' à partir de la ligne de commande.",
"diagnosis_ip_global": "IP globale : {global}
",
"diagnosis_ip_local": "IP locale : {local}
",
- "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation sur https://yunohost.org/dns_config si vous avez besoin d’aide pour configurer les enregistrements DNS.",
- "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains fournisseurs ne vous laisseront pas débloquer le port sortant 25 parce qu’ils ne se soucient pas de la neutralité du Net.
- Certains d’entre eux offrent l’alternative d'utiliser un serveur de messagerie relai bien que cela implique que le relai sera en mesure d’espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net",
- "diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des courriels!",
- "diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de l’extérieur en IPv{ipversion}. Il ne pourra pas recevoir des courriels.",
+ "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation sur https://yunohost.org/dns_config si vous avez besoin d'aide pour configurer les enregistrements DNS.",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains fournisseurs ne vous laisseront pas débloquer le port sortant 25 parce qu'ils ne se soucient pas de la neutralité du Net.
- Certains d'entre eux offrent l'alternative d'utiliser un serveur de messagerie relai bien que cela implique que le relai sera en mesure d'espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net",
+ "diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des emails !",
+ "diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de l'extérieur en IPv{ipversion}. Il ne pourra pas recevoir des emails.",
"diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes: assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.",
- "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu: {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante ce problème est que le port 25 n’est pas correctement redirigé vers votre serveur . Vous pouvez également vous assurer qu’aucun pare-feu ou proxy inversé n’interfère.",
- "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée …). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI fournissent l’alternative de à l’aide d’un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer de fournisseur",
- "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée ...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'e-mails en exécutant yunohost settings set smtp.allow_ipv6 -v off . Remarque: cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir de courriels avec les quelques serveurs qui ont uniquement de l'IPv6.",
+ "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu : {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante à ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur. Vous pouvez également vous assurer qu'aucun pare-feu ou reverse-proxy n'interfère.",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI fournissent l'alternative de à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer de fournisseur",
+ "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set smtp.allow_ipv6 -v off . Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir de emails avec les quelques serveurs qui ont uniquement de l'IPv6.",
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverse actuel : {rdns_domain}
Valeur attendue : {ehlo_domain}
",
"diagnosis_mail_blacklist_listed_by": "Votre IP ou domaine {item}
est sur liste noire sur {blacklist_name}",
- "diagnosis_mail_queue_unavailable": "Impossible de consulter le nombre d’e-mails en attente dans la file d'attente",
+ "diagnosis_mail_queue_unavailable": "Impossible de consulter le nombre d'emails en attente dans la file d'attente",
"diagnosis_ports_partially_unreachable": "Le port {port} n'est pas accessible de l'extérieur en IPv{failed}.",
"diagnosis_http_hairpinning_issue": "Votre réseau local ne semble pas supporter l'hairpinning.",
"diagnosis_http_hairpinning_issue_details": "C'est probablement à cause de la box/routeur de votre fournisseur d'accès internet. Par conséquent, les personnes extérieures à votre réseau local pourront accéder à votre serveur comme prévu, mais pas les personnes internes au réseau local (comme vous, probablement ?) si elles utilisent le nom de domaine ou l'IP globale. Vous pourrez peut-être améliorer la situation en consultant https://yunohost.org/dns_local_network",
- "diagnosis_http_partially_unreachable": "Le domaine {domain} semble inaccessible en HTTP depuis l’extérieur du réseau local en IPv{failed}, bien qu’il fonctionne en IPv{passed}.",
+ "diagnosis_http_partially_unreachable": "Le domaine {domain} semble inaccessible en HTTP depuis l'extérieur du réseau local en IPv{failed}, bien qu'il fonctionne en IPv{passed}.",
"diagnosis_http_nginx_conf_not_up_to_date": "La configuration Nginx de ce domaine semble avoir été modifiée manuellement et empêche YunoHost de diagnostiquer si elle est accessible en HTTP.",
- "diagnosis_http_nginx_conf_not_up_to_date_details": "Pour corriger la situation, inspectez la différence avec la ligne de commande en utilisant les outils yunohost tools regen-conf nginx --dry-run --with-diff et si vous êtes d’accord, appliquez les modifications avec yunohost tools regen-conf nginx --force .",
+ "diagnosis_http_nginx_conf_not_up_to_date_details": "Pour corriger la situation, inspectez la différence avec la ligne de commande en utilisant les outils yunohost tools regen-conf nginx --dry-run --with-diff et si vous êtes d'accord, appliquez les modifications avec yunohost tools regen-conf nginx --force .",
"backup_archive_cant_retrieve_info_json": "Impossible d'avoir des informations sur l'archive '{archive}'... Le fichier info.json ne peut pas être trouvé (ou n'est pas un fichier json valide).",
"backup_archive_corrupted": "Il semble que l'archive de la sauvegarde '{archive}' est corrompue : {error}",
- "diagnosis_ip_no_ipv6_tip": "L'utilisation de IPv6 n'est pas obligatoire pour le fonctionnement de votre serveur, mais cela contribue à la santé d'Internet dans son ensemble. IPv6 généralement configuré automatiquement par votre système ou votre FAI s'il est disponible. Autrement, vous devrez prendre quelque minutes pour le configurer manuellement à l'aide de cette documentation: https://yunohost.org/#/ipv6. Si vous ne pouvez pas activer IPv6 ou si c'est trop technique pour vous, vous pouvez aussi ignorer cet avertissement sans que cela pose problème.",
+ "diagnosis_ip_no_ipv6_tip": "L'utilisation de IPv6 n'est pas obligatoire pour le fonctionnement de votre serveur, mais cela contribue à la santé d'Internet dans son ensemble. IPv6 généralement configuré automatiquement par votre système ou votre FAI s'il est disponible. Autrement, vous devrez prendre quelque minutes pour le configurer manuellement à l'aide de cette documentation : https://yunohost.org/#/ipv6. Si vous ne pouvez pas activer IPv6 ou si c'est trop technique pour vous, vous pouvez aussi ignorer cet avertissement sans que cela pose problème.",
"diagnosis_domain_expiration_not_found": "Impossible de vérifier la date d'expiration de certains domaines",
- "diagnosis_domain_expiration_not_found_details": "Les informations WHOIS pour le domaine {domain} ne semblent pas contenir les informations concernant la date d'expiration ?",
+ "diagnosis_domain_expiration_not_found_details": "Les informations WHOIS pour le domaine {domain} ne semblent pas contenir les informations concernant la date d'expiration ?",
"diagnosis_domain_not_found_details": "Le domaine {domain} n'existe pas dans la base de donnée WHOIS ou est expiré !",
"diagnosis_domain_expiration_success": "Vos domaines sont enregistrés et ne vont pas expirer prochainement.",
"diagnosis_domain_expiration_warning": "Certains domaines vont expirer prochainement !",
"diagnosis_domain_expiration_error": "Certains domaines vont expirer TRÈS PROCHAINEMENT !",
"diagnosis_domain_expires_in": "{domain} expire dans {days} jours.",
"certmanager_domain_not_diagnosed_yet": "Il n'y a pas encore de résultat de diagnostic pour le domaine {domain}. Merci de relancer un diagnostic pour les catégories 'Enregistrements DNS' et 'Web' dans la section Diagnostique pour vérifier si le domaine est prêt pour Let's Encrypt. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)",
- "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement l’espérance de vie du périphérique.",
+ "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement l'espérance de vie du périphérique.",
"restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}",
"regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.",
- "migration_0015_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus utiles...",
+ "migration_0015_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires ...",
"migration_0015_specific_upgrade": "Démarrage de la mise à jour des paquets du système qui doivent être mis à jour séparément...",
"migration_0015_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}",
"migration_0015_problematic_apps_warning": "Veuillez noter que des applications qui peuvent poser problèmes ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost, ou bien qu'elles ne soient pas signalées comme \"fonctionnelles\". Par conséquent, il n'est pas possible de garantir que les applications suivantes fonctionneront encore après la mise à niveau : {problematic_apps}",
- "migration_0015_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n…- de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https://yunohost.org/backup ;\n…- d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.",
+ "migration_0015_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n...- de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https://yunohost.org/backup ;\n...- d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.",
"migration_0015_system_not_fully_up_to_date": "Votre système n'est pas entièrement à jour. Veuillez effectuer une mise à jour normale avant de lancer la migration vers Buster.",
"migration_0015_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devriez avoir au moins 1 Go de libre pour effectuer cette migration.",
"migration_0015_not_stretch": "La distribution Debian actuelle n'est pas Stretch !",
- "migration_0015_yunohost_upgrade": "Démarrage de la mise à jour de YunoHost...",
+ "migration_0015_yunohost_upgrade": "Démarrage de la mise à jour de YunoHost ...",
"migration_0015_still_on_stretch_after_main_upgrade": "Quelque chose s'est mal passé lors de la mise à niveau, le système semble toujours être sous Debian Stretch",
"migration_0015_main_upgrade": "Démarrage de la mise à niveau générale...",
"migration_0015_patching_sources_list": "Mise à jour du fichier sources.lists...",
"migration_0015_start": "Démarrage de la migration vers Buster",
"migration_description_0015_migrate_to_buster": "Mise à niveau du système vers Debian Buster et YunoHost 4.x",
- "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par Yunohost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force .",
+ "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant yunohost dyndns update --force .",
"app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.",
"migration_0015_weak_certs": "Il a été constaté que les certificats suivants utilisent encore des algorithmes de signature peu robustes et doivent être mis à jour pour être compatibles avec la prochaine version de NGINX : {certs}",
- "global_settings_setting_backup_compress_tar_archives": "Compresser les archives (.tar.gz) au lieu des archives non-compressées lors de la création des backups. N.B. : activer cette option permet d'obtenir des sauvegardes plus légères, mais leur création sera significativement plus longue et plus gourmande en CPU.",
+ "global_settings_setting_backup_compress_tar_archives": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.",
"migration_description_0018_xtable_to_nftable": "Migrer les anciennes règles de trafic réseau vers le nouveau système basé sur nftables",
"service_description_php7.3-fpm": "Exécute les applications écrites en PHP avec NGINX",
"migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable par défaut a échoué : {error}",
- "migration_0018_failed_to_migrate_iptables_rules": "La migration des règles iptables héritées vers nftables a échoué: {error}",
+ "migration_0018_failed_to_migrate_iptables_rules": "Échec de la migration des anciennes règles iptables vers nftables : {error}",
"migration_0017_not_enough_space": "Laissez suffisamment d'espace disponible dans {path} avant de lancer la migration.",
"migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 est installé mais pas posgreSQL 11 ? Il s'est sans doute passé quelque chose d'étrange sur votre système :(...",
"migration_0017_postgresql_96_not_installed": "PostgreSQL n'a pas été installé sur votre système. Aucune opération à effectuer.",
@@ -661,36 +581,133 @@
"app_manifest_install_ask_is_public": "Cette application devrait-elle être visible par les visiteurs anonymes ?",
"app_manifest_install_ask_admin": "Choisissez un administrateur pour cette application",
"app_manifest_install_ask_password": "Choisissez un mot de passe administrateur pour cette application",
- "app_manifest_install_ask_path": "Choisissez le chemin sur lequel vous souhaitez installer cette application",
+ "app_manifest_install_ask_path": "Choisissez le chemin d'URL (après le domaine) où cette application doit être installée",
"app_manifest_install_ask_domain": "Choisissez le domaine sur lequel vous souhaitez installer cette application",
- "global_settings_setting_smtp_relay_user": "Relais de compte utilisateur SMTP",
- "global_settings_setting_smtp_relay_port": "Port relais SMTP",
- "global_settings_setting_smtp_relay_host": "Relais SMTP à utiliser pour envoyer du courrier à la place de cette instance YunoHost. Utile si vous êtes dans l'une de ces situations : votre port 25 est bloqué par votre FAI ou votre fournisseur VPS, vous avez une IP résidentielle répertoriée sur DUHL, vous ne pouvez pas configurer de DNS inversé ou ce serveur n'est pas directement exposé sur Internet et vous voulez en utiliser un autre pour envoyer des mails.",
- "diagnosis_package_installed_from_sury_details": "Certains paquets ont été installés par inadvertance à partir d'un dépôt tiers appelé Sury. L'équipe YunoHost a amélioré la stratégie de gestion de ces paquets, mais on s'attend à ce que certaines configurations qui ont installé des applications PHP7.3 tout en étant toujours sur Stretch présentent des incohérences. Pour résoudre cette situation, vous devez essayer d'exécuter la commande suivante : {cmd_to_fix} ",
+ "global_settings_setting_smtp_relay_user": "Compte utilisateur du relais SMTP",
+ "global_settings_setting_smtp_relay_port": "Port du relais SMTP",
+ "global_settings_setting_smtp_relay_host": "Un relais SMTP permet d'envoyer du courrier à la place de cette instance YunoHost. Cela est utile si vous êtes dans l'une de ces situations : le port 25 est bloqué par votre FAI ou par votre fournisseur VPS, vous avez une IP résidentielle répertoriée sur DUHL, vous ne pouvez pas configurer de reverse DNS ou le serveur n'est pas directement accessible depuis Internet et que vous voulez en utiliser un autre pour envoyer des mails.",
+ "diagnosis_package_installed_from_sury_details": "Certains paquets ont été installés par inadvertance à partir d'un dépôt tiers appelé Sury. L'équipe YunoHost a amélioré la stratégie de gestion de ces paquets, mais on s'attend à ce que certaines configurations qui ont installé des applications PHP7.3 tout en étant toujours sur Stretch présentent des incohérences. Pour résoudre cette situation, vous devez essayer d'exécuter la commande suivante : {cmd_to_fix} ",
"app_argument_password_no_default": "Erreur lors de l'analyse de l'argument de mot de passe '{name}' : l'argument de mot de passe ne peut pas avoir de valeur par défaut pour des raisons de sécurité",
- "pattern_email_forward": "Il doit s'agir d'une adresse électronique valide, le symbole '+' étant accepté (par exemples : johndoe@exemple.com ou bien johndoe+yunohost@exemple.com)",
+ "pattern_email_forward": "L'adresse électronique doit être valide, le symbole '+' étant accepté (par exemple : johndoe+yunohost@exemple.com)",
"global_settings_setting_smtp_relay_password": "Mot de passe du relais de l'hôte SMTP",
"diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version",
- "additional_urls_already_added": "URL supplémentaire '{url:s}' déjà ajoutée pour la permission '{permission:s}'",
+ "additional_urls_already_added": "URL supplémentaire '{url}' déjà ajoutée pour la permission '{permission}'",
"unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.",
- "show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, car l'URL de l'autorisation '{permission}' est une expression régulière",
+ "show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, cela car l'URL de l'autorisation '{permission}' est une expression régulière",
"show_tile_cant_be_enabled_for_url_not_defined": "Vous ne pouvez pas activer 'show_tile' pour le moment, car vous devez d'abord définir une URL pour l'autorisation '{permission}'",
"regex_with_only_domain": "Vous ne pouvez pas utiliser une expression régulière pour le domaine, uniquement pour le chemin",
"regex_incompatible_with_tile": "/!\\ Packagers ! La permission '{permission}' a 'show_tile' définie sur 'true' et vous ne pouvez donc pas définir une URL regex comme URL principale",
"permission_protected": "L'autorisation {permission} est protégée. Vous ne pouvez pas ajouter ou supprimer le groupe visiteurs à/de cette autorisation.",
"migration_0019_slapd_config_will_be_overwritten": "Il semble que vous ayez modifié manuellement la configuration de slapd. Pour cette migration critique, YunoHost doit forcer la mise à jour de la configuration slapd. Les fichiers originaux seront sauvegardés dans {conf_backup_folder}.",
- "migration_0019_migration_failed_trying_to_rollback": "Impossible de migrer... tentative de restauration du système.",
- "migration_0019_can_not_backup_before_migration": "La sauvegarde du système n'a pas pu être effectuée avant l'échec de la migration. Erreur : {error:s}",
- "migration_0019_backup_before_migration": "Création d'une sauvegarde de la base de données LDAP et des paramètres des applications avant la migration.",
"migration_0019_add_new_attributes_in_ldap": "Ajouter de nouveaux attributs pour les autorisations dans la base de données LDAP",
"migrating_legacy_permission_settings": "Migration des anciens paramètres d'autorisation...",
- "invalid_regex": "Regex non valide : '{regex:s}'",
+ "invalid_regex": "Regex non valide : '{regex}'",
"domain_name_unknown": "Domaine '{domain}' inconnu",
"app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.",
- "additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimées pour la permission '{permission:s}'",
- "migration_0019_rollback_success": "Retour à l'état antérieur du système.",
+ "additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'",
"invalid_number": "Doit être un nombre",
"migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives",
- "diagnosis_basesystem_hardware_model": "Le modèle du serveur est {model}",
- "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système."
+ "diagnosis_basesystem_hardware_model": "Le modèle/architecture du serveur est {model}",
+ "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.",
+ "postinstall_low_rootfsspace": "Le système de fichiers a une taille totale inférieure à 10 Go, ce qui est préoccupant et devrait attirer votre attention ! Vous allez certainement arriver à court d'espace disque (très) rapidement ! Il est recommandé d'avoir au moins 16 Go à la racine pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez la post-installation avec --force-diskspace",
+ "domain_remove_confirm_apps_removal": "Le retrait de ce domaine retirera aussi ces applications :\n{apps}\n\nÊtes vous sûr de vouloir cela ? [{answers}]",
+ "diagnosis_rootfstotalspace_critical": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement le remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.",
+ "diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Cela peut suffire, mais faites attention car vous risquez de les remplir rapidement... Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.",
+ "app_restore_script_failed": "Une erreur s'est produite dans le script de restauration de l'application",
+ "restore_backup_too_old": "Cette sauvegarde ne peut pas être restaurée car elle provient d'une version trop ancienne de YunoHost.",
+ "migration_update_LDAP_schema": "Mise à jour du schéma LDAP...",
+ "log_backup_create": "Créer une archive de sauvegarde",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "Activer la superposition de la vignette SSOwat",
+ "migration_ldap_rollback_success": "Système rétabli dans son état initial.",
+ "permission_cant_add_to_all_users": "L'autorisation {permission} ne peut pas être ajoutée à tous les utilisateurs.",
+ "migration_ldap_migration_failed_trying_to_rollback": "Impossible de migrer... tentative de restauration du système.",
+ "migration_ldap_can_not_backup_before_migration": "La sauvegarde du système n'a pas pu être terminée avant l'échec de la migration. Erreur : {error }",
+ "migration_ldap_backup_before_migration": "Création d'une sauvegarde de la base de données LDAP et des paramètres des applications avant la migration proprement dite.",
+ "migration_description_0020_ssh_sftp_permissions": "Ajouter la prise en charge des autorisations SSH et SFTP",
+ "global_settings_setting_security_ssh_port": "Port SSH",
+ "diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter yunohost settings set security.ssh.port -v VOTRE_PORT_SSH pour définir le port SSH, et vérifiez yunohost tools regen-conf ssh --dry-run --with-diff et yunohost tools regen-conf ssh --force pour réinitialiser votre configuration aux recommandations YunoHost.",
+ "diagnosis_sshd_config_inconsistent": "Il semble que le port SSH a été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.port' est disponible pour éviter de modifier manuellement la configuration.",
+ "diagnosis_sshd_config_insecure": "La configuration SSH semble avoir été modifiée manuellement et n'est pas sécurisée car elle ne contient aucune directive 'AllowGroups' ou 'AllowUsers' pour limiter l'accès aux utilisateurs autorisés.",
+ "backup_create_size_estimation": "L'archive contiendra environ {size} de données.",
+ "global_settings_setting_security_webadmin_allowlist": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.",
+ "global_settings_setting_security_webadmin_allowlist_enabled": "Autoriser seulement certaines IP à accéder à la webadmin.",
+ "diagnosis_http_localdomain": "Le domaine {domain}, avec un TLD .local, ne devrait pas être exposé en dehors du réseau local.",
+ "diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial comme .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.",
+ "invalid_password": "Mot de passe incorrect",
+ "ldap_server_is_down_restart_it": "Le service LDAP est en panne, essayez de le redémarrer...",
+ "ldap_server_down": "Impossible d'atteindre le serveur LDAP",
+ "global_settings_setting_security_experimental_enabled": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)",
+ "diagnosis_apps_deprecated_practices": "La version installée de cette application utilise toujours certaines pratiques de packaging obsolètes. Vous devriez vraiment envisager de mettre l'application à jour.",
+ "diagnosis_apps_outdated_ynh_requirement": "La version installée de cette application nécessite uniquement YunoHost >= 2.x, cela indique que l'application n'est pas à jour avec les bonnes pratiques de packaging et les helpers recommandées. Vous devriez vraiment envisager de mettre l'application à jour.",
+ "diagnosis_apps_bad_quality": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.",
+ "diagnosis_apps_broken": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.",
+ "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",
+ "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",
+ "diagnosis_high_number_auth_failures": "Il y a eu récemment un grand nombre d'échecs d'authentification. Assurez-vous que Fail2Ban est en cours d'exécution et est correctement configuré, ou utilisez un port personnalisé pour SSH comme expliqué dans https://yunohost.org/security.",
+ "global_settings_setting_security_nginx_redirect_to_https": "Rediriger les requêtes HTTP vers HTTPS par défaut (NE PAS DÉSACTIVER à moins de savoir vraiment ce que vous faites !)",
+ "config_validate_color": "Doit être une couleur hexadécimale RVB valide",
+ "app_config_unable_to_apply": "Échec de l'application des valeurs du panneau de configuration.",
+ "app_config_unable_to_read": "Échec de la lecture des valeurs du panneau de configuration.",
+ "config_apply_failed": "Échec de l'application de la nouvelle configuration : {error}",
+ "config_cant_set_value_on_section": "Vous ne pouvez pas définir une seule valeur sur une section de configuration entière.",
+ "config_forbidden_keyword": "Le mot-clé '{keyword}' est réservé, vous ne pouvez pas créer ou utiliser un panneau de configuration avec une question avec cet identifiant.",
+ "config_no_panel": "Aucun panneau de configuration trouvé.",
+ "config_unknown_filter_key": "La clé de filtre '{filter_key}' est incorrecte.",
+ "config_validate_date": "Doit être une date valide comme dans le format AAAA-MM-JJ",
+ "config_validate_email": "Doit être un email valide",
+ "config_validate_time": "Doit être une heure valide comme HH:MM",
+ "config_validate_url": "Doit être une URL Web valide",
+ "config_version_not_supported": "Les versions du panneau de configuration '{version}' ne sont pas prises en charge.",
+ "danger": "Danger :",
+ "file_extension_not_accepted": "Le fichier '{path}' est refusé car son extension ne fait pas partie des extensions acceptées : {accept}",
+ "invalid_number_min": "Doit être supérieur à {min}",
+ "invalid_number_max": "Doit être inférieur à {max}",
+ "log_app_config_set": "Appliquer la configuration à l'application '{}'",
+ "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}",
+ "app_argument_password_help_keep": "Tapez sur Entrée pour conserver la valeur actuelle",
+ "app_argument_password_help_optional": "Tapez un espace pour vider le mot de passe",
+ "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.",
+ "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.",
+ "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')",
+ "domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )",
+ "domain_config_features_disclaimer": "Jusqu'à présent, l'activation/désactivation des fonctionnalités de messagerie ou XMPP n'a d'impact que sur la configuration DNS recommandée et automatique, et non sur les configurations système !",
+ "domain_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.",
+ "domain_dns_registrar_managed_in_parent_domain": "Ce domaine est un sous-domaine de {parent_domain_link}. La configuration du registrar DNS doit être gérée dans le panneau de configuration de {parent_domain}.",
+ "domain_dns_registrar_not_supported": "YunoHost n'a pas pu détecter automatiquement le bureau d'enregistrement gérant ce domaine. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns.",
+ "domain_dns_registrar_experimental": "Jusqu'à présent, l'interface avec l'API de **{registrar}** n'a pas été correctement testée et revue par la communauté YunoHost. L'assistance est **très expérimentale** - soyez prudent !",
+ "domain_dns_push_failed_to_authenticate": "Échec de l'authentification sur l'API du bureau d'enregistrement pour le domaine « {domain} ». Très probablement les informations d'identification sont incorrectes ? (Erreur : {error})",
+ "domain_dns_push_failed_to_list": "Échec de la liste des enregistrements actuels à l'aide de l'API du registraire : {error}",
+ "domain_dns_push_already_up_to_date": "Dossiers déjà à jour.",
+ "domain_dns_pushing": "Transmission des enregistrements DNS...",
+ "domain_dns_push_record_failed": "Échec de l'enregistrement {action} {type}/{name} : {error}",
+ "domain_dns_push_success": "Enregistrements DNS mis à jour !",
+ "domain_dns_push_failed": "La mise à jour des enregistrements DNS a échoué.",
+ "domain_dns_push_partial_failure": "Enregistrements DNS partiellement mis à jour : certains avertissements/erreurs ont été signalés.",
+ "domain_config_mail_in": "Emails entrants",
+ "domain_config_mail_out": "Emails sortants",
+ "domain_config_xmpp": "Messagerie instantanée (XMPP)",
+ "domain_config_auth_token": "Jeton d'authentification",
+ "domain_config_auth_key": "Clé d'authentification",
+ "domain_config_auth_secret": "Secret d'authentification",
+ "domain_config_api_protocol": "Protocole API",
+ "domain_config_auth_entrypoint": "Point d'entrée API",
+ "domain_config_auth_application_key": "Clé d'application",
+ "domain_config_auth_application_secret": "Clé secrète de l'application",
+ "ldap_attribute_already_exists": "L'attribut LDAP '{attribute}' existe déjà avec la valeur '{value}'",
+ "log_domain_config_set": "Mettre à jour la configuration du domaine '{}'",
+ "log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'",
+ "diagnosis_http_special_use_tld": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et n'est donc pas censé être exposé en dehors du réseau local.",
+ "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.",
+ "other_available_options": "... et {n} autres options disponibles non affichées",
+ "domain_config_auth_consumer_key": "Consumer key"
}
diff --git a/locales/gl.json b/locales/gl.json
new file mode 100644
index 000000000..987093df8
--- /dev/null
+++ b/locales/gl.json
@@ -0,0 +1,713 @@
+{
+ "password_too_simple_1": "O contrasinal ten que ter 8 caracteres como mínimo",
+ "aborting": "Abortando.",
+ "app_already_up_to_date": "{app} xa está actualizada",
+ "app_already_installed_cant_change_url": "Esta app xa está instalada. O URL non pode cambiarse só con esta acción. Miran en `app changeurl` se está dispoñible.",
+ "app_already_installed": "{app} xa está instalada",
+ "app_action_broke_system": "Esta acción semella que estragou estos servizos importantes: {services}",
+ "app_action_cannot_be_ran_because_required_services_down": "Estos servizos requeridos deberían estar en execución para realizar esta acción: {services}. Intenta reinicialos para continuar (e tamén intenta saber por que están apagados).",
+ "already_up_to_date": "Nada que facer. Todo está ao día.",
+ "admin_password_too_long": "Elixe un contrasinal menor de 127 caracteres",
+ "admin_password_changed": "Realizado o cambio de contrasinal de administración",
+ "admin_password_change_failed": "Non se puido cambiar o contrasinal",
+ "admin_password": "Contrasinal de administración",
+ "additional_urls_already_removed": "URL adicional '{url}' xa foi eliminada das URL adicionais para o permiso '{permission}'",
+ "additional_urls_already_added": "URL adicional '{url}' xa fora engadida ás URL adicionais para o permiso '{permission}'",
+ "action_invalid": "Acción non válida '{action}'",
+ "app_argument_required": "Requírese o argumento '{name}'",
+ "app_argument_password_no_default": "Erro ao procesar o argumento do contrasinal '{name}': o argumento do contrasinal non pode ter un valor por defecto por razón de seguridade",
+ "app_argument_invalid": "Elixe un valor válido para o argumento '{name}': {error}",
+ "app_argument_choice_invalid": "Elixe un valor válido para o argumento '{name}': '{value}' non está entre as opcións dispoñibles ({choices})",
+ "backup_archive_writing_error": "Non se puideron engadir os ficheiros '{source}' (chamados no arquivo '{dest}' para ser copiados dentro do arquivo comprimido '{archive}'",
+ "backup_archive_system_part_not_available": "A parte do sistema '{part}' non está dispoñible nesta copia",
+ "backup_archive_corrupted": "Semella que o arquivo de copia '{archive}' está estragado : {error}",
+ "backup_archive_cant_retrieve_info_json": "Non se puido cargar a info desde arquivo '{archive}'... O info.json non s puido obter (ou é un json non válido).",
+ "backup_archive_open_failed": "Non se puido abrir o arquivo de copia de apoio",
+ "backup_archive_name_unknown": "Arquivo local de copia de apoio descoñecido con nome '{name}'",
+ "backup_archive_name_exists": "Xa existe un arquivo de copia con este nome.",
+ "backup_archive_broken_link": "Non se puido acceder ao arquivo da copia (ligazón rota a {path})",
+ "backup_archive_app_not_found": "Non se atopa {app} no arquivo da copia",
+ "backup_applying_method_tar": "Creando o arquivo TAR da copia...",
+ "backup_applying_method_custom": "Chamando polo método de copia de apoio personalizado '{method}'...",
+ "backup_applying_method_copy": "Copiando tódolos ficheiros necesarios...",
+ "backup_app_failed": "Non se fixo copia de {app}",
+ "backup_actually_backuping": "Creando o arquivo de copia cos ficheiros recollidos...",
+ "backup_abstract_method": "Este método de copia de apoio aínda non foi implementado",
+ "ask_password": "Contrasinal",
+ "ask_new_path": "Nova ruta",
+ "ask_new_domain": "Novo dominio",
+ "ask_new_admin_password": "Novo contrasinal de administración",
+ "ask_main_domain": "Dominio principal",
+ "ask_lastname": "Apelido",
+ "ask_firstname": "Nome",
+ "ask_user_domain": "Dominio a utilizar como enderezo de email e conta XMPP da usuaria",
+ "apps_catalog_update_success": "O catálogo de aplicacións foi actualizado!",
+ "apps_catalog_obsolete_cache": "A caché do catálogo de apps está baleiro ou obsoleto.",
+ "apps_catalog_failed_to_download": "Non se puido descargar o catálogo de apps {apps_catalog}: {error}",
+ "apps_catalog_updating": "Actualizando o catálogo de aplicacións...",
+ "apps_catalog_init_success": "Sistema do catálogo de apps iniciado!",
+ "apps_already_up_to_date": "Xa tes tódalas apps ao día",
+ "app_packaging_format_not_supported": "Esta app non se pode instalar porque o formato de empaquetado non está soportado pola túa versión de YunoHost. Deberías considerar actualizar o teu sistema.",
+ "app_upgraded": "{app} actualizadas",
+ "app_upgrade_some_app_failed": "Algunhas apps non se puideron actualizar",
+ "app_upgrade_script_failed": "Houbo un fallo interno no script de actualización da app",
+ "app_upgrade_failed": "Non se actualizou {app}: {error}",
+ "app_upgrade_app_name": "Actualizando {app}...",
+ "app_upgrade_several_apps": "Vanse actualizar as seguintes apps: {apps}",
+ "app_unsupported_remote_type": "Tipo remoto non soportado para a app",
+ "app_unknown": "App descoñecida",
+ "app_start_restore": "Restaurando {app}...",
+ "app_start_backup": "Xuntando os ficheiros para a copia de apoio de {app}...",
+ "app_start_remove": "Eliminando {app}...",
+ "app_start_install": "Instalando {app}...",
+ "app_sources_fetch_failed": "Non se puideron obter os ficheiros fonte, é o URL correcto?",
+ "app_restore_script_failed": "Houbo un erro interno do script de restablecemento da app",
+ "app_restore_failed": "Non se puido restablecer {app}: {error}",
+ "app_remove_after_failed_install": "Eliminando a app debido ao fallo na instalación...",
+ "app_requirements_unmeet": "Non se cumpren os requerimentos de {app}, o paquete {pkgname} ({version}) debe ser {spec}",
+ "app_requirements_checking": "Comprobando os paquetes requeridos por {app}...",
+ "app_removed": "{app} desinstalada",
+ "app_not_properly_removed": "{app} non se eliminou de xeito correcto",
+ "app_not_installed": "Non se puido atopar {app} na lista de apps instaladas: {all_apps}",
+ "app_not_correctly_installed": "{app} semella que non está instalada correctamente",
+ "app_not_upgraded": "Fallou a actualización da app '{failed_app}', como consecuencia as actualizacións das seguintes apps foron canceladas: {apps}",
+ "app_manifest_install_ask_is_public": "Debería esta app estar exposta ante visitantes anónimas?",
+ "app_manifest_install_ask_admin": "Elixe unha usuaria administradora para esta app",
+ "app_manifest_install_ask_password": "Elixe un contrasinal de administración para esta app",
+ "app_manifest_install_ask_path": "Elixe a ruta URL (após o dominio) onde será instalada esta app",
+ "app_manifest_install_ask_domain": "Elixe o dominio onde queres instalar esta app",
+ "app_manifest_invalid": "Hai algún erro no manifesto da app: {error}",
+ "app_location_unavailable": "Este URL ou ben non está dispoñible ou entra en conflito cunha app(s) xa instalada:\n{apps}",
+ "app_label_deprecated": "Este comando está anticuado! Utiliza o novo comando 'yunohost user permission update' para xestionar a etiqueta da app.",
+ "app_make_default_location_already_used": "Non se puido establecer a '{app}' como app por defecto no dominio, '{domain}' xa está utilizado por '{other_app}'",
+ "app_install_script_failed": "Houbo un fallo interno do script de instalación da app",
+ "app_install_failed": "Non se pode instalar {app}: {error}",
+ "app_install_files_invalid": "Non se poden instalar estos ficheiros",
+ "app_id_invalid": "ID da app non válido",
+ "app_full_domain_unavailable": "Lamentámolo, esta app ten que ser instalada nun dominio propio, pero xa tes outras apps instaladas no dominio '{domain}'. Podes usar un subdominio dedicado para esta app.",
+ "app_extraction_failed": "Non se puideron extraer os ficheiros de instalación",
+ "app_change_url_success": "A URL de {app} agora é {domain}{path}",
+ "app_change_url_no_script": "A app '{app_name}' non soporta o cambio de URL. Pode que debas actualizala.",
+ "app_change_url_identical_domains": "O antigo e o novo dominio/url_path son idénticos ('{domain}{path}'), nada que facer.",
+ "backup_deleted": "Copia de apoio eliminada",
+ "backup_delete_error": "Non se eliminou '{path}'",
+ "backup_custom_mount_error": "O método personalizado de copia non superou o paso 'mount'",
+ "backup_custom_backup_error": "O método personalizado da copia non superou o paso 'backup'",
+ "backup_csv_creation_failed": "Non se creou o ficheiro CSV necesario para restablecer a copia",
+ "backup_csv_addition_failed": "Non se engadiron os ficheiros a copiar ao ficheiro CSV",
+ "backup_creation_failed": "Non se puido crear o arquivo de copia de apoio",
+ "backup_create_size_estimation": "O arquivo vai conter arredor de {size} de datos.",
+ "backup_created": "Copia de apoio creada",
+ "backup_couldnt_bind": "Non se puido ligar {src} a {dest}.",
+ "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo",
+ "backup_cleaning_failed": "Non se puido baleirar o cartafol temporal para a copia",
+ "backup_cant_mount_uncompress_archive": "Non se puido montar o arquivo sen comprimir porque está protexido contra escritura",
+ "backup_ask_for_copying_if_needed": "Queres realizar a copia de apoio utilizando temporalmente {size}MB? (Faise deste xeito porque algúns ficheiros non hai xeito de preparalos usando unha forma máis eficiente.)",
+ "backup_running_hooks": "Executando os ganchos da copia...",
+ "backup_permission": "Permiso de copia para {app}",
+ "backup_output_symlink_dir_broken": "O directorio de arquivo '{path}' é unha ligazón simbólica rota. Pode ser que esqueceses re/montar ou conectar o medio de almacenaxe ao que apunta.",
+ "backup_output_directory_required": "Debes proporcionar un directorio de saída para a copia",
+ "backup_output_directory_not_empty": "Debes elexir un directorio de saída baleiro",
+ "backup_output_directory_forbidden": "Elixe un directorio de saída diferente. As copias non poden crearse en /bin, /boot, /dev, /etc, /lib, /root, /sbin, /sys, /usr, /var ou subcartafoles de /home/yunohost.backup/archives",
+ "backup_nothings_done": "Nada que gardar",
+ "backup_no_uncompress_archive_dir": "Non hai tal directorio do arquivo descomprimido",
+ "backup_mount_archive_for_restore": "Preparando o arquivo para restauración...",
+ "backup_method_tar_finished": "Creouse o arquivo de copia TAR",
+ "backup_method_custom_finished": "O método de copia personalizado '{method}' rematou",
+ "backup_method_copy_finished": "Rematou o copiado dos ficheiros",
+ "backup_hook_unknown": "O gancho da copia '{hook}' é descoñecido",
+ "certmanager_domain_cert_not_selfsigned": "O certificado para o dominio {domain} non está auto-asinado. Tes a certeza de querer substituílo? (Usa '--force' para facelo.)",
+ "certmanager_domain_not_diagnosed_yet": "Por agora non hai resultado de diagnóstico para o dominio {domain}. Volve facer o diagnóstico para a categoría 'Rexistros DNS' e 'Web' na sección de diagnóstico para comprobar se o dominio é compatible con Let's Encrypt. (Ou se sabes o que estás a facer, usa '--no-checks' para desactivar esas comprobacións.)",
+ "certmanager_certificate_fetching_or_enabling_failed": "Fallou o intento de usar o novo certificado para '{domain}'...",
+ "certmanager_cert_signing_failed": "Non se puido asinar o novo certificado",
+ "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o dominio '{domain}'",
+ "certmanager_cert_install_success_selfsigned": "O certificado auto-asinado está instalado para o dominio '{domain}'",
+ "certmanager_cert_install_success": "O certificado Let's Encrypt está instalado para o dominio '{domain}'",
+ "certmanager_cannot_read_cert": "Algo fallou ao intentar abrir o certificado actual para o dominio {domain} (ficheiro: {file}), razón: {reason}",
+ "certmanager_attempt_to_replace_valid_cert": "Estás intentando sobrescribir un certificado correcto e en bo estado para o dominio {domain}! (Usa --force para obviar)",
+ "certmanager_attempt_to_renew_valid_cert": "O certificado para o dominio '{domain}' non caduca pronto! (Podes usar --force se sabes o que estás a facer)",
+ "certmanager_attempt_to_renew_nonLE_cert": "O certificado para o dominio '{domain}' non está proporcionado por Let's Encrypt. Non se pode renovar automáticamente!",
+ "certmanager_acme_not_configured_for_domain": "Non se realizou o desafío ACME para {domain} porque a súa configuración nginx non ten a parte do código correspondente... Comproba que a túa configuración nginx está ao día utilizando `yunohost tools regen-conf nginx --dry-run --with-diff`.",
+ "backup_with_no_restore_script_for_app": "'{app}' non ten script de restablecemento, non poderás restablecer automáticamente a copia de apoio desta app.",
+ "backup_with_no_backup_script_for_app": "A app '{app}' non ten script para a copia. Ignorada.",
+ "backup_unable_to_organize_files": "Non se puido usar o método rápido para organizar ficheiros no arquivo",
+ "backup_system_part_failed": "Non se puido facer copia da parte do sistema '{part}'",
+ "certmanager_domain_http_not_working": "O dominio {domain} semella non ser accesible a través de HTTP. Comproba a categoría 'Web' no diagnóstico para máis info. (Se sabes o que estás a facer, utiliza '--no-checks' para obviar estas comprobacións.)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "Os rexistros DNS para o dominio '{domain}' son diferentes aos da IP deste servidor. Comproba a categoría 'Rexistros DNS' (básico) no diagnóstico para ter máis info. Se cambiaches recentemente o rexistro A, agarda a que se propague o cambio (están dispoñibles ferramentas en liña para comprobar estos cambios). (Se sabes o que estás a facer, utiliza '--no-checks' para obviar estas comprobacións.)",
+ "confirm_app_install_danger": "PERIGO! Esta app aínda é experimental (pode que nin funcione)! Probablemente NON deberías instalala a non ser que sepas o que estás a facer. NON TERÁS SOPORTE nin axuda se esta app estraga o teu sistema... Se queres asumir o risco, escribe '{answers}'",
+ "confirm_app_install_warning": "Aviso: Esta app podería funcionar, pero non está ben integrada en YunoHost. Algunhas funcións como a identificación centralizada e as copias de apoio poderían non estar dispoñibles. Desexas instalala igualmente? [{answers}] ",
+ "certmanager_unable_to_parse_self_CA_name": "Non se puido obter o nome da autoridade do auto-asinado (ficheiro: {file})",
+ "certmanager_self_ca_conf_file_not_found": "Non se atopa o ficheiro de configuración para a autoridade de auto-asinado (ficheiro: {file})",
+ "certmanager_no_cert_file": "Non se puido ler o ficheiro do certificado para o dominio {domain} (ficheiro: {file})",
+ "certmanager_hit_rate_limit": "Recentemente crearonse demasiados certificados para este mesmo grupo de dominios {domain}. Inténtao máis tarde. Podes ler https://letsencrypt.org/docs/rate-limits/ para máis info",
+ "certmanager_warning_subdomain_dns_record": "O subdominio '{subdomain}' non resolve a mesmo enderezo IP que '{domain}'. Algunhas funcións non estarán dispoñibles ata que arranxes isto e rexeneres o certificado.",
+ "diagnosis_found_errors_and_warnings": "Atopado(s) {errors} problema(s) significativo(s) (e {warnings} avisos(s)) en relación a {category}!",
+ "diagnosis_found_errors": "Atopado(s) {errors} problema significativo(s) relacionado con {category}!",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} problema ignorado(s))",
+ "diagnosis_cant_run_because_of_dep": "Non é posible facer o diganóstico para {category} cando aínda hai importantes problemas con {dep}.",
+ "diagnosis_cache_still_valid": "(A caché aínda é válida para o diagnóstico {category}. Non o repetiremos polo de agora!)",
+ "diagnosis_failed_for_category": "O diagnóstico fallou para a categoría '{category}': {error}",
+ "diagnosis_display_tip": "Para ver os problemas atopados, podes ir á sección de Diagnóstico na administración web, ou executa 'yunohost diagnosis show --issues --human-readable' desde a liña de comandos.",
+ "diagnosis_package_installed_from_sury_details": "Algúns paquetes foron instalados se darse conta desde un repositorio de terceiros chamado Sury. O equipo de YunoHost mellorou a estratexia para xestionar estos paquetes, pero é de agardar que algunhas instalacións que instalaron aplicacións PHP7.3 estando aínda en Stretch teñan inconsistencias co sistema. Para arranxar esta situación, deberías intentar executar o comando: {cmd_to_fix} ",
+ "diagnosis_package_installed_from_sury": "Algúns paquetes do sistema deberían ser baixados de versión",
+ "diagnosis_backports_in_sources_list": "Semella que apt (o xestor de paquetes) está configurado para usar o repositorio backports. A non ser que saibas o que fas NON che recomendamos instalar paquetes desde backports, porque é probable que produzas inestabilidades e conflitos no teu sistema.",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Estás executando versións inconsistentes de paquetes YunoHost... probablemente debido a actualizacións parciais ou fallidas.",
+ "diagnosis_basesystem_ynh_main_version": "O servidor está a executar Yunohost {main_version} ({repo})",
+ "diagnosis_basesystem_ynh_single_version": "{package} versión: {version} ({repo})",
+ "diagnosis_basesystem_kernel": "O servidor está a executar o kernel Linux {kernel_version}",
+ "diagnosis_basesystem_host": "O servidor está a executar Debian {debian_version}",
+ "diagnosis_basesystem_hardware_model": "O modelo de servidor é {model}",
+ "diagnosis_basesystem_hardware": "A arquitectura do hardware do servidor é {virt} {arch}",
+ "custom_app_url_required": "Tes que proporcionar o URL para actualizar a app personalizada {app}",
+ "confirm_app_install_thirdparty": "PERIGO! Esta app non forma parte do catálogo de YunoHost. Ao instalar apps de terceiros poderías comprometer a integridade e seguridade do sistema. Probablemente NON deberías instalala a menos que saibas o que fas. NON SE PROPORCIONARÁ SOPORTE se esta app non funciona ou estraga o sistema... Se aínda así asumes o risco, escribe '{answers}'",
+ "diagnosis_dns_point_to_doc": "Revisa a documentación en https://yunohost.org/dns_config se precisas axuda para configurar os rexistros DNS.",
+ "diagnosis_dns_discrepancy": "O seguinte rexistro DNS non segue a configuración recomendada:
Tipo: {type}
Nome: {name}
Valor actual: {current}
Valor agardado: {value}
",
+ "diagnosis_dns_missing_record": "Facendo caso á configuración DNS recomendada, deberías engadir un rexistro DNS coa seguinte info.
Tipo: {type}
Nome: {name}
Valor: {value}
",
+ "diagnosis_dns_bad_conf": "Faltan algúns rexistros DNS ou están mal configurados para o dominio {domain} (categoría {category})",
+ "diagnosis_dns_good_conf": "Os rexistros DNS están correctamente configurados para o dominio {domain} (categoría {category})",
+ "diagnosis_ip_weird_resolvconf_details": "O ficheiro /etc/resolv.conf
debería ser unha ligazón simbólica a /etc/resolvconf/run/resolv.conf
apuntando el mesmo a 127.0.0.1
(dnsmasq). Se queres configurar manualmente a resolución DNS, por favor edita /etc/resolv.dnsmasq.conf
.",
+ "diagnosis_ip_weird_resolvconf": "A resolución DNS semella funcionar, mais parecese que estás a utilizar un /etc/resolv.conf
personalizado.",
+ "diagnosis_ip_broken_resolvconf": "A resolución de nomes de dominio semella non funcionar no teu servidor, que parece ter relación con que /etc/resolv.conf
non sinala a 127.0.0.1
.",
+ "diagnosis_ip_broken_dnsresolution": "A resolución de nomes de dominio semella que por algunha razón non funciona... Pode estar o cortalumes bloqueando as peticións DNS?",
+ "diagnosis_ip_dnsresolution_working": "A resolución de nomes de dominio está a funcionar!",
+ "diagnosis_ip_not_connected_at_all": "O servidor semella non ter ningún tipo de conexión a internet!?",
+ "diagnosis_ip_local": "IP local: {local}
",
+ "diagnosis_ip_global": "IP global: {global}
",
+ "diagnosis_ip_no_ipv6_tip": "Que o servidor teña conexión IPv6 non é obrigatorio para que funcione, pero é mellor para o funcionamento de Internet en conxunto. IPv6 debería estar configurado automáticamente no teu sistema ou provedor se está dispoñible. Doutro xeito, poderías ter que configurar manualmente algúns parámetros tal como se explica na documentación: https://yunohost.org/#/ipv6. Se non podes activar IPv6 ou é moi complicado para ti, podes ignorar tranquilamente esta mensaxe.",
+ "diagnosis_ip_no_ipv6": "O servidor non ten conexión IPv6.",
+ "diagnosis_ip_connected_ipv6": "O servidor está conectado a internet a través de IPv6!",
+ "diagnosis_ip_no_ipv4": "O servidor non ten conexión IPv4.",
+ "diagnosis_ip_connected_ipv4": "O servidor está conectado a internet a través de IPv4!",
+ "diagnosis_no_cache": "Aínda non hai datos na caché para '{category}'",
+ "diagnosis_failed": "Non se puido obter o resultado do diagnóstico para '{category}': {error}",
+ "diagnosis_everything_ok": "Semella todo correcto en {category}!",
+ "diagnosis_found_warnings": "Atoparonse {warnings} elemento(s) que poderían optimizarse en {category}.",
+ "diagnosis_services_bad_status": "O servizo {service} está {status} :(",
+ "diagnosis_services_conf_broken": "A configuración do {service} está estragada!",
+ "diagnosis_services_running": "O servizo {service} está en execución!",
+ "diagnosis_domain_expires_in": "{domain} caduca en {days} días.",
+ "diagnosis_domain_expiration_error": "Algúns dominios van caducan MOI PRONTO!",
+ "diagnosis_domain_expiration_warning": "Algúns dominios van caducar pronto!",
+ "diagnosis_domain_expiration_success": "Os teus dominios están rexistrados e non van caducar pronto.",
+ "diagnosis_domain_expiration_not_found_details": "A información WHOIS para o dominio {domain} non semella conter información acerca da data de caducidade?",
+ "diagnosis_domain_not_found_details": "O dominio {domain} non existe na base de datos de WHOIS ou está caducado!",
+ "diagnosis_domain_expiration_not_found": "Non se puido comprobar a data de caducidade para algúns dominios",
+ "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost dyndns update --force .",
+ "diagnosis_swap_ok": "O sistema ten {total} de swap!",
+ "diagnosis_swap_notsomuch": "O sistema só ten {total} de swap. Deberías considerar ter polo menos {recommended} para evitar situacións onde o sistema esgote a memoria.",
+ "diagnosis_swap_none": "O sistema non ten partición swap. Deberías considerar engadir polo menos {recommended} de swap para evitar situación onde o sistema esgote a memoria.",
+ "diagnosis_ram_ok": "Ao sistema aínda lle queda {available} ({available_percent}%) de RAM dispoñible dun total de {total}.",
+ "diagnosis_ram_low": "O sistema ten {available} ({available_percent}%) da RAM dispoñible (total {total}). Ten coidado.",
+ "diagnosis_ram_verylow": "Ao sistema só lle queda {available} ({available_percent}%) de RAM dispoñible! (total {total})",
+ "diagnosis_diskusage_ok": "A almacenaxe {mountpoint}
(no dispositivo {device}
) aínda ten {free} ({free_percent}%) de espazo restante (de {total})!",
+ "diagnosis_diskusage_low": "A almacenaxe {mountpoint}
(no dispositivo {device}
) só lle queda {free} ({free_percent}%) de espazo libre (de {total}). Ten coidado.",
+ "diagnosis_diskusage_verylow": "A almacenaxe {mountpoint}
(no dispositivo {device}
) só lle queda {free} ({free_percent}%) de espazo libre (de {total}). Deberías considerar liberar algún espazo!",
+ "diagnosis_services_bad_status_tip": "Podes intentar reiniciar o servizo, e se isto non funciona, mira os rexistros do servizo na webadmin (desde a liña de comandos con yunohost service restart {service} e yunohost service log {service} ).",
+ "diagnosis_mail_outgoing_port_25_ok": "O servidor de email SMTP pode enviar emails (porto 25 de saída non está bloqueado).",
+ "diagnosis_swap_tip": "Por favor ten en conta que se o servidor ten a swap instalada nunha tarxeta SD ou almacenaxe SSD podería reducir drásticamente a expectativa de vida do dispositivo.",
+ "diagnosis_mail_outgoing_port_25_blocked": "O servidor SMTP de email non pode enviar emails a outros servidores porque o porto saínte 25 está bloqueado en IPv{ipversion}.",
+ "diagnosis_mail_ehlo_unreachable": "O servidor de email SMTP non é accesible desde o exterior en IPv{ipversion}. Non poderá recibir emails.",
+ "diagnosis_mail_ehlo_ok": "O servidor de email SMTP é accesible desde o exterior e por tanto pode recibir emails!",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algúns provedores non che van permitir desbloquear o porto 25 saínte porque non se preocupan pola Neutralidade da Rede.
- Algúns deles dan unha alternativa usando un repetidor de servidor de email mais isto implica que o repetidor poderá espiar todo o teu tráfico de email.
- Unha alternativa é utilizar unha VPN *cun IP público dedicado* para evitar este tipo de limitación. Le https://yunohost.org/#/vpn_advantage
- Tamén podes considerar cambiar a un provedor máis amigable coa neutralidade da rede",
+ "diagnosis_mail_outgoing_port_25_blocked_details": "Antes deberías intentar desbloquear o porto 25 saínte no teu rúter de internet ou na web do provedor de hospedaxe. (Algúns provedores poderían pedirche que fagas unha solicitude para isto).",
+ "diagnosis_mail_ehlo_wrong": "Un servidor de email SMPT diferente responde en IPv{ipversion}. O teu servidor probablemente non poida recibir emails.",
+ "diagnosis_mail_ehlo_bad_answer_details": "Podería deberse a que outro servidor está a responder no lugar do teu.",
+ "diagnosis_mail_ehlo_bad_answer": "Un servizo non-SMTP respondeu no porto 25 en IPv{ipversion}",
+ "diagnosis_mail_ehlo_unreachable_details": "Non se puido abrir unha conexión no porto 25 do teu servidor en IPv{ipversion}. Non semella accesible.
1. A causa máis habitual é que o porto 25 non está correctamente redirixido no servidor.
2. Asegúrate tamén de que o servizo postfix está a funcionar.
3. En configuracións máis complexas: asegúrate de que o cortalumes ou reverse-proxy non están interferindo.",
+ "diagnosis_mail_fcrdns_nok_details": "Deberías intentar configurar o DNS inverso con {ehlo_domain}
na interface do teu rúter de internet ou na interface do teu provedor de hospedaxe. (Algúns provedores de hospedaxe poderían pedirche que lle fagas unha solicitude por escrito para isto).",
+ "diagnosis_mail_fcrdns_dns_missing": "Non hai DNS inverso definido en IPv{ipversion}. Algúns emails poderían non ser entregrado ou ser marcados como spam.",
+ "diagnosis_mail_fcrdns_ok": "O DNS inverso está correctamente configurado!",
+ "diagnosis_mail_ehlo_could_not_diagnose_details": "Erro: {error}",
+ "diagnosis_mail_ehlo_could_not_diagnose": "Non se puido determinar se o servidor de email postfix é accesible desde o exterior en IPv{ipversion}.",
+ "diagnosis_mail_ehlo_wrong_details": "O EHLO recibido polo diagnosticador remoto en IPv{ipversion} é diferente ao dominio do teu servidor.
EHLO recibido: {wrong_ehlo}
Agardado: {right_ehlo}
A razón máis habitual para este problema é que o porto 25 non está correctamente redirixido ao teu servidor. Alternativamente, asegúrate de non ter un cortalumes ou reverse-proxy interferindo.",
+ "diagnosis_regenconf_manually_modified_details": "Probablemente todo sexa correcto se sabes o que estás a facer! YunoHost non vai actualizar este ficheiro automáticamente... Pero ten en conta que as actualizacións de YunoHost poderían incluír cambios importantes recomendados. Se queres podes ver as diferenzas con yunohost tools regen-conf {category} --dry-run --with-diff e forzar o restablecemento da configuración recomendada con yunohost tools regen-conf {category} --force ",
+ "diagnosis_regenconf_manually_modified": "O ficheiro de configuración {file}
semella que foi modificado manualmente.",
+ "diagnosis_regenconf_allgood": "Tódolos ficheiros de configuración seguen a configuración recomendada!",
+ "diagnosis_mail_queue_too_big": "Hai demasiados emails pendentes na cola de correo ({nb_pending} emails)",
+ "diagnosis_mail_queue_unavailable_details": "Erro: {error}",
+ "diagnosis_mail_queue_unavailable": "Non se pode consultar o número de emails pendentes na cola",
+ "diagnosis_mail_queue_ok": "{nb_pending} emails pendentes na cola de correo",
+ "diagnosis_mail_blacklist_website": "Tras ver a razón do bloqueo e arranxalo, considera solicitar que o teu dominio ou IP sexan eliminados de {blacklist_website}",
+ "diagnosis_mail_blacklist_reason": "A razón do bloqueo é: {reason}",
+ "diagnosis_mail_blacklist_listed_by": "O teu dominio ou IP {item}
está na lista de bloqueo {blacklist_name}",
+ "diagnosis_mail_blacklist_ok": "Os IPs e dominios utilizados neste servidor non parecen estar en listas de bloqueo",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverso actual: {rdns_domain}
Valor agardado: {ehlo_domain}
",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain": "O DNS inverso non está correctamente configurado para IPv{ipversion}. É posible que non se entreguen algúns emails ou sexan marcados como spam.",
+ "diagnosis_mail_fcrdns_nok_alternatives_6": "Algúns provedores non che permiten configurar DNS inverso (ou podería non funcionar...). Se o teu DNS inverso está correctamente configurado para IPv4, podes intentar desactivar o uso de IPv6 ao enviar os emails executando yunohost settings set smtp.allow_ipv6 -v off . Nota: esta última solución significa que non poderás enviar ou recibir emails desde os poucos servidores que só usan IPv6 que teñen esta limitación.",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Algúns provedores non che permiten configurar o teu DNS inverso (ou podería non ser funcional...). Se tes problemas debido a isto, considera as seguintes solucións:
- Algúns ISP proporcionan alternativas como usar un repetidor de servidor de correo pero implica que o repetidor pode ver todo o teu tráfico de email.
-Unha alternativa respetuosa coa privacidade é utilizar un VPN *cun IP público dedicado* para evitar estas limitacións. Le https://yunohost.org/#/vpn_advantage
- Ou tamén podes cambiar a un provedor diferente",
+ "diagnosis_http_ok": "O dominio {domain} é accesible a través de HTTP desde o exterior da rede local.",
+ "diagnosis_http_could_not_diagnose_details": "Erro: {error}",
+ "diagnosis_http_could_not_diagnose": "Non se puido comprobar se os dominios son accesibles desde o exterior en IPv{ipversion}.",
+ "diagnosis_http_hairpinning_issue_details": "Isto acontece probablemente debido ao rúter do teu ISP. Como resultado, as persoas externas á túa rede local poderán acceder ao teu servidor tal como se espera, pero non as usuarias na rede local (como ti, probablemente?) cando usan o nome de dominio ou IP global. Podes mellorar a situación lendo https://yunohost.org/dns_local_network",
+ "diagnosis_http_hairpinning_issue": "A túa rede local semella que non ten hairpinning activado.",
+ "diagnosis_ports_forwarding_tip": "Para arranxar isto, probablemente tes que configurar o reenvío do porto no teu rúter de internet tal como se di en https://yunohost.org/isp_box_config",
+ "diagnosis_ports_needed_by": "A apertura deste porto é precisa para {category} (servizo {service})",
+ "diagnosis_ports_ok": "O porto {port} é accesible desde o exterior.",
+ "diagnosis_ports_partially_unreachable": "O porto {port} non é accesible desde o exterior en IPv{failed}.",
+ "diagnosis_ports_unreachable": "O porto {port} non é accesible desde o exterior.",
+ "diagnosis_ports_could_not_diagnose_details": "Erro: {error}",
+ "diagnosis_ports_could_not_diagnose": "Non se puido comprobar se os portos son accesibles desde o exterior en IPv{ipversion}.",
+ "diagnosis_description_regenconf": "Configuracións do sistema",
+ "diagnosis_description_mail": "Email",
+ "diagnosis_description_web": "Web",
+ "diagnosis_description_ports": "Exposición de portos",
+ "diagnosis_description_systemresources": "Recursos do sistema",
+ "diagnosis_description_services": "Comprobación do estado dos servizos",
+ "diagnosis_description_dnsrecords": "Rexistros DNS",
+ "diagnosis_description_ip": "Conectividade a internet",
+ "diagnosis_description_basesystem": "Sistema base",
+ "diagnosis_security_vulnerable_to_meltdown_details": "Para arranxar isto, deberías actualizar o sistema e reiniciar para cargar o novo kernel linux (ou contactar co provedor do servizo se isto non o soluciona). Le https://meltdownattack.com/ para máis info.",
+ "diagnosis_security_vulnerable_to_meltdown": "Semella que es vulnerable á vulnerabilidade crítica de seguridade Meltdown",
+ "diagnosis_rootfstotalspace_critical": "O sistema de ficheiros root só ten un total de {space} e podería ser preocupante! Probablemente esgotes o espazo no disco moi pronto! Recomendamos ter un sistema de ficheiros root de polo menos 16 GB.",
+ "diagnosis_rootfstotalspace_warning": "O sistema de ficheiros root só ten un total de {space}. Podería ser suficiente, mais pon tino porque poderías esgotar o espazo no disco rápidamente... Recoméndase ter polo meno 16 GB para o sistema de ficheiros root.",
+ "domain_cannot_remove_main": "Non podes eliminar '{domain}' porque é o dominio principal, primeiro tes que establecer outro dominio como principal usando 'yunohost domain main-domain -n '; aquí tes a lista dos dominios posibles: {other_domains}",
+ "diagnosis_sshd_config_inconsistent_details": "Executa yunohost settings set security.ssh.port -v O_TEU_PORTO_SSH para definir o porto SSH, comproba con yunohost tools regen-conf ssh --dry-run --with-diff e restablece a configuración con yunohost tools regen-conf ssh --force a configuración recomendada de YunoHost.",
+ "diagnosis_sshd_config_inconsistent": "Semella que o porto SSH foi modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, un novo axuste global 'security.ssh.port' está dispoñible para evitar a edición manual da configuración.",
+ "diagnosis_sshd_config_insecure": "Semella que a configuración SSH modificouse manualmente, e é insegura porque non contén unha directiva 'AllowGroups' ou 'AllowUsers' para limitar o acceso ás usuarias autorizadas.",
+ "diagnosis_processes_killed_by_oom_reaper": "Algúns procesos foron apagados recentemente polo sistema porque quedou sen memoria dispoñible. Isto acontece normalmente porque o sistema quedou sen memoria ou un proceso consumía demasiada. Resumo cos procesos apagados:\n{kills_summary}",
+ "diagnosis_never_ran_yet": "Semella que o servidor foi configurado recentemente e aínda non hai informes diagnósticos. Deberías iniciar un diagnóstico completo, ben desde a administración web ou usando 'yunohost diagnosis run' desde a liña de comandos.",
+ "diagnosis_unknown_categories": "As seguintes categorías son descoñecidas: {categories}",
+ "diagnosis_http_nginx_conf_not_up_to_date_details": "Para arranxar a situación, revisa as diferenzas na liña de comandos usando yunohost tools regen-conf nginx --dry-run --with-diff e se todo está ben, aplica os cambios con yunohost tools regen-conf nginx --force .",
+ "diagnosis_http_nginx_conf_not_up_to_date": "A configuración nginx deste dominio semella foi modificada manualmente, e está evitando que YunoHost comprobe se é accesible a través de HTTP.",
+ "diagnosis_http_partially_unreachable": "O dominio {domain} non semella accesible a través de HTTP desde o exterior da rede local en IPv{failed}, pero funciona en IPv{passed}.",
+ "diagnosis_http_unreachable": "O dominio {domain} non semella accesible a través de HTTP desde o exterior da rede local.",
+ "diagnosis_http_bad_status_code": "Semella que outra máquina (podería ser o rúter de internet) respondeu no lugar do teu servidor.
1. A razón máis habitual para este problema é que o porto 80 (e 443) non están correctamente redirixidos ao teu servidor.
2. En configuracións avanzadas: revisa que nin o cortalumes nin o proxy-inverso están interferindo.",
+ "diagnosis_http_connection_error": "Erro de conexión: non se puido conectar co dominio solicitado, moi probablemente non sexa accesible.",
+ "diagnosis_http_timeout": "Caducou a conexión mentras se intentaba contactar o servidor desde o exterior. Non semella accesible.
1. A razón máis habitual é que o porto 80 (e 443) non están correctamente redirixidos ao teu servidor.
2. Deberías comprobar tamén que o servizo nginx está a funcionar
3. En configuracións máis avanzadas: revisa que nin o cortalumes nin o proxy-inverso están interferindo.",
+ "field_invalid": "Campo non válido '{}'",
+ "experimental_feature": "Aviso: esta característica é experimental e non se considera estable, non deberías utilizala a menos que saibas o que estás a facer.",
+ "extracting": "Extraendo...",
+ "dyndns_unavailable": "O dominio '{domain}' non está dispoñible.",
+ "dyndns_domain_not_provided": "O provedor DynDNS {provider} non pode proporcionar o dominio {domain}.",
+ "dyndns_registration_failed": "Non se rexistrou o dominio DynDNS: {error}",
+ "dyndns_registered": "Dominio DynDNS rexistrado",
+ "dyndns_provider_unreachable": "Non se puido acadar o provedor DynDNS {provider}: pode que o teu YunoHost non teña conexión a internet ou que o servidor dynette non funcione.",
+ "dyndns_no_domain_registered": "Non hai dominio rexistrado con DynDNS",
+ "dyndns_key_not_found": "Non se atopou a chave DNS para o dominio",
+ "dyndns_key_generating": "Creando chave DNS... podería demorarse.",
+ "dyndns_ip_updated": "Actualizouse o IP en DynDNS",
+ "dyndns_ip_update_failed": "Non se actualizou o enderezo IP en DynDNS",
+ "dyndns_could_not_check_available": "Non se comprobou se {domain} está dispoñible en {provider}.",
+ "dyndns_could_not_check_provide": "Non se comprobou se {provider} pode proporcionar {domain}.",
+ "dpkg_lock_not_available": "Non se pode executar agora mesmo este comando porque semella que outro programa está a utilizar dpkg (o xestos de paquetes do sistema)",
+ "dpkg_is_broken": "Non podes facer isto agora mesmo porque dpkg/APT (o xestor de paquetes do sistema) semella que non está a funcionar... Podes intentar solucionalo conectándote a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.",
+ "downloading": "Descargando...",
+ "done": "Feito",
+ "domains_available": "Dominios dispoñibles:",
+ "domain_name_unknown": "Dominio '{domain}' descoñecido",
+ "domain_uninstall_app_first": "Aínda están instaladas estas aplicacións no teu dominio:\n{apps}\n\nPrimeiro desinstalaas utilizando 'yunohost app remove id_da_app' ou móveas a outro dominio con 'yunohost app change-url id_da_app' antes de eliminar o dominio",
+ "domain_remove_confirm_apps_removal": "Ao eliminar o dominio tamén vas eliminar estas aplicacións:\n{apps}\n\nTes a certeza de querer facelo? [{answers}]",
+ "domain_hostname_failed": "Non se puido establecer o novo nome de servidor. Esto pode causar problemas máis tarde (tamén podería ser correcto).",
+ "domain_exists": "Xa existe o dominio",
+ "domain_dyndns_root_unknown": "Dominio raiz DynDNS descoñecido",
+ "domain_dyndns_already_subscribed": "Xa tes unha subscrición a un dominio DynDNS",
+ "domain_dns_conf_is_just_a_recommendation": "Este comando móstrache a configuración *recomendada*. Non realiza a configuración DNS no teu nome. É responsabilidade túa configurar as zonas DNS no servizo da empresa que xestiona o rexistro do dominio seguindo esta recomendación.",
+ "domain_deletion_failed": "Non se puido eliminar o dominio {domain}: {error}",
+ "domain_deleted": "Dominio eliminado",
+ "domain_creation_failed": "Non se puido crear o dominio {domain}: {error}",
+ "domain_created": "Dominio creado",
+ "domain_cert_gen_failed": "Non se puido crear o certificado",
+ "domain_cannot_remove_main_add_new_one": "Non podes eliminar '{domain}' porque é o dominio principal e único dominio, primeiro tes que engadir outro dominio usando 'yunohost domain add ', e despois establecelo como o dominio principal utilizando 'yunohost domain main-domain -n ' e entón poderás eliminar '{domain}' con 'yunohost domain remove {domain}'.'",
+ "domain_cannot_add_xmpp_upload": "Non podes engadir dominios que comecen con 'xmpp-upload.'. Este tipo de nome está reservado para a función se subida de XMPP integrada en YunoHost.",
+ "file_does_not_exist": "O ficheiro {path} non existe.",
+ "firewall_reload_failed": "Non se puido recargar o cortalumes",
+ "global_settings_setting_smtp_allow_ipv6": "Permitir o uso de IPv6 para recibir e enviar emais",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "Activar as capas no panel SSOwat",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Permitir o uso de DSA hostkey (en desuso) para a configuración do demoño SSH",
+ "global_settings_unknown_setting_from_settings_file": "Chave descoñecida nos axustes: '{setting_key}', descártaa e gárdaa en /etc/yunohost/settings-unknown.json",
+ "global_settings_setting_security_ssh_port": "Porto SSH",
+ "global_settings_setting_security_postfix_compatibility": "Compromiso entre compatibilidade e seguridade para o servidor Postfix. Aféctalle ao cifrado (e outros aspectos da seguridade)",
+ "global_settings_setting_security_ssh_compatibility": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade)",
+ "global_settings_setting_security_password_user_strength": "Fortaleza do contrasinal da usuaria",
+ "global_settings_setting_security_password_admin_strength": "Fortaleza do contrasinal de Admin",
+ "global_settings_setting_security_nginx_compatibility": "Compromiso entre compatiblidade e seguridade para o servidor NGINX. Afecta ao cifrado (e outros aspectos relacionados coa seguridade)",
+ "global_settings_setting_pop3_enabled": "Activar protocolo POP3 no servidor de email",
+ "global_settings_reset_success": "Fíxose copia de apoio dos axustes en {path}",
+ "global_settings_key_doesnt_exists": "O axuste '{settings_key}' non existe nos axustes globais, podes ver os valores dispoñibles executando 'yunohost settings list'",
+ "global_settings_cant_write_settings": "Non se gardou o ficheiro de configuración, razón: {reason}",
+ "global_settings_cant_serialize_settings": "Non se serializaron os datos da configuración, razón: {reason}",
+ "global_settings_cant_open_settings": "Non se puido abrir o ficheiro de axustes, razón: {reason}",
+ "global_settings_bad_type_for_setting": "Tipo incorrecto do axuste {setting}, recibido {received_type}, agardábase {expected_type}",
+ "global_settings_bad_choice_for_enum": "Elección incorrecta para o axuste {setting}, recibido '{choice}', mais as opcións dispoñibles son: {available_choices}",
+ "firewall_rules_cmd_failed": "Fallou algún comando das regras do cortalumes. Máis info no rexistro.",
+ "firewall_reloaded": "Recargouse o cortalumes",
+ "group_creation_failed": "Non se puido crear o grupo '{group}': {error}",
+ "group_created": "Creouse o grupo '{group}'",
+ "group_already_exist_on_system_but_removing_it": "O grupo {group} xa é un dos grupos do sistema, pero YunoHost vaino eliminar...",
+ "group_already_exist_on_system": "O grupo {group} xa é un dos grupos do sistema",
+ "group_already_exist": "Xa existe o grupo {group}",
+ "good_practices_about_user_password": "Vas definir o novo contrasinal de usuaria. O contrasinal debe ter 8 caracteres como mínimo—aínda que se recomenda utilizar un máis longo (ex. unha frase de paso) e/ou utilizar caracteres variados (maiúsculas, minúsculas, números e caracteres especiais).",
+ "good_practices_about_admin_password": "Vas definir o novo contrasinal de administración. O contrasinal debe ter 8 caracteres como mínimo—aínda que se recomenda utilizar un máis longo (ex. unha frase de paso) e/ou utilizar caracteres variados (maiúsculas, minúsculas, números e caracteres especiais).",
+ "global_settings_unknown_type": "Situación non agardada, o axuste {setting} semella ter o tipo {unknown_type} pero non é un valor soportado polo sistema.",
+ "global_settings_setting_backup_compress_tar_archives": "Ao crear novas copias de apoio, comprime os arquivos (.tar.gz) en lugar de non facelo (.tar). Nota: activando esta opción creas arquivos máis lixeiros, mais o procedemento da primeira copia será significativamente máis longo e esixente coa CPU.",
+ "global_settings_setting_smtp_relay_password": "Contrasinal no repetidor SMTP",
+ "global_settings_setting_smtp_relay_user": "Conta de usuaria no repetidor SMTP",
+ "global_settings_setting_smtp_relay_port": "Porto do repetidor SMTP",
+ "global_settings_setting_smtp_relay_host": "Servidor repetidor SMTP para enviar emails no lugar da túa instancia yunohost. É útil se estás nunha destas situacións: o teu porto 25 está bloqueado polo teu provedor ISP u VPN, se tes unha IP residencial nunha lista DUHL, se non podes configurar DNS inversa ou se este servidor non ten conexión directa a internet e queres utilizar outro para enviar os emails.",
+ "group_updated": "Grupo '{group}' actualizado",
+ "group_unknown": "Grupo descoñecido '{group}'",
+ "group_deletion_failed": "Non se eliminou o grupo '{group}': {error}",
+ "group_deleted": "Grupo '{group}' eliminado",
+ "group_cannot_be_deleted": "O grupo {group} non se pode eliminar manualmente.",
+ "group_cannot_edit_primary_group": "O grupo '{group}' non se pode editar manualmente. É o grupo primario que contén só a unha usuaria concreta.",
+ "group_cannot_edit_visitors": "O grupo 'visitors' non se pode editar manualmente. É un grupo especial que representa a tódas visitantes anónimas",
+ "group_cannot_edit_all_users": "O grupo 'all_users' non se pode editar manualmente. É un grupo especial que contén tódalas usuarias rexistradas en YunoHost",
+ "global_settings_setting_security_webadmin_allowlist": "Enderezos IP con permiso para acceder á webadmin. Separados por vírgulas.",
+ "global_settings_setting_security_webadmin_allowlist_enabled": "Permitir que só algúns IPs accedan á webadmin.",
+ "disk_space_not_sufficient_update": "Non hai espazo suficiente no disco para actualizar esta aplicación",
+ "disk_space_not_sufficient_install": "Non queda espazo suficiente no disco para instalar esta aplicación",
+ "log_help_to_get_log": "Para ver o rexistro completo da operación '{desc}', usa o comando 'yunohost log show {name}'",
+ "log_link_to_log": "Rexistro completo desta operación: '{desc}'",
+ "log_corrupted_md_file": "O ficheiro YAML con metadatos asociado aos rexistros está danado: '{md_file}\nErro: {error}'",
+ "iptables_unavailable": "Non podes andar remexendo en iptables aquí. Ou ben estás nun contedor ou o teu kernel non ten soporte para isto",
+ "ip6tables_unavailable": "Non podes remexer en ip6tables aquí. Ou ben estás nun contedor ou o teu kernel non ten soporte para isto",
+ "invalid_regex": "Regex non válido: '{regex}'",
+ "installation_complete": "Instalación completa",
+ "hook_name_unknown": "Nome descoñecido do gancho '{name}'",
+ "hook_list_by_invalid": "Esta propiedade non se pode usar para enumerar os ganchos",
+ "hook_json_return_error": "Non se puido ler a info de retorno do gancho {path}. Erro: {msg}. Contido en bruto: {raw_content}",
+ "hook_exec_not_terminated": "O script non rematou correctamente: {path}",
+ "hook_exec_failed": "Non se executou o script: {path}",
+ "group_user_not_in_group": "A usuaria {user} non está no grupo {group}",
+ "group_user_already_in_group": "A usuaria {user} xa está no grupo {group}",
+ "group_update_failed": "Non se actualizou o grupo '{group}': {error}",
+ "log_permission_delete": "Eliminar permiso '{}'",
+ "log_permission_create": "Crear permiso '{}'",
+ "log_letsencrypt_cert_install": "Instalar un certificado Let's Encrypt para o dominio '{}'",
+ "log_dyndns_update": "Actualizar o IP asociado ao teu subdominio YunoHost '{}'",
+ "log_dyndns_subscribe": "Subscribirse a un subdominio YunoHost '{}'",
+ "log_domain_remove": "Eliminar o dominio '{}' da configuración do sistema",
+ "log_domain_add": "Engadir dominio '{}' á configuración do sistema",
+ "log_remove_on_failed_install": "Eliminar '{}' tras unha instalación fallida",
+ "log_remove_on_failed_restore": "Eliminar '{}' tras un intento fallido de restablecemento desde copia",
+ "log_backup_restore_app": "Restablecer '{}' desde unha copia de apoio",
+ "log_backup_restore_system": "Restablecer o sistema desde unha copia de apoio",
+ "log_backup_create": "Crear copia de apoio",
+ "log_available_on_yunopaste": "Este rexistro está dispoñible en {url}",
+ "log_app_action_run": "Executar acción da app '{}'",
+ "log_app_makedefault": "Converter '{}' na app por defecto",
+ "log_app_upgrade": "Actualizar a app '{}'",
+ "log_app_remove": "Eliminar a app '{}'",
+ "log_app_install": "Instalar a app '{}'",
+ "log_app_change_url": "Cambiar o URL da app '{}'",
+ "log_operation_unit_unclosed_properly": "Non se pechou correctamente a unidade da operación",
+ "log_does_exists": "Non hai rexistro de operación co nome '{log}', usa 'yunohost log list' para ver tódolos rexistros de operacións dispoñibles",
+ "log_help_to_get_failed_log": "A operación '{desc}' non se completou. Comparte o rexistro completo da operación utilizando o comando 'yunohost log share {name}' para obter axuda",
+ "log_link_to_failed_log": "Non se completou a operación '{desc}'. Por favor envía o rexistro completo desta operación premendo aquí para obter axuda",
+ "migration_0015_start": "Comezando a migración a Buster",
+ "migration_update_LDAP_schema": "Actualizando esquema LDAP...",
+ "migration_ldap_rollback_success": "Sistema restablecido.",
+ "migration_ldap_migration_failed_trying_to_rollback": "Non se puido migrar... intentando volver á versión anterior do sistema.",
+ "migration_ldap_can_not_backup_before_migration": "O sistema de copia de apoio do sistema non se completou antes de que fallase a migración. Erro: {error}",
+ "migration_ldap_backup_before_migration": "Crear copia de apoio da base de datos LDAP e axustes de apps antes de realizar a migración.",
+ "migration_description_0020_ssh_sftp_permissions": "Engadir soporte para permisos SSH e SFTP",
+ "migration_description_0019_extend_permissions_features": "Extender/recrear o sistema de xestión de permisos de apps",
+ "migration_description_0018_xtable_to_nftable": "Migrar as regras de tráfico de rede antigas ao novo sistema nftable",
+ "migration_description_0017_postgresql_9p6_to_11": "Migrar bases de datos desde PostgreSQL 9.6 a 11",
+ "migration_description_0016_php70_to_php73_pools": "Migrar o ficheiros de configuración 'pool' de php7.0-fpm a php7.3",
+ "migration_description_0015_migrate_to_buster": "Actualizar o sistema a Debian Buster e YunoHost 4.x",
+ "migrating_legacy_permission_settings": "Migrando os axustes dos permisos anteriores...",
+ "main_domain_changed": "Foi cambiado o dominio principal",
+ "main_domain_change_failed": "Non se pode cambiar o dominio principal",
+ "mail_unavailable": "Este enderezo de email está reservado e debería adxudicarse automáticamente á primeira usuaria",
+ "mailbox_used_space_dovecot_down": "O servizo de caixa de correo Dovecot ten que estar activo se queres obter o espazo utilizado polo correo",
+ "mailbox_disabled": "Desactivado email para usuaria {user}",
+ "mail_forward_remove_failed": "Non se eliminou o reenvío de email '{mail}'",
+ "mail_domain_unknown": "Enderezo de email non válido para o dominio '{domain}'. Usa un dominio administrado por este servidor.",
+ "mail_alias_remove_failed": "Non se puido eliminar o alias de email '{mail}'",
+ "log_tools_reboot": "Reiniciar o servidor",
+ "log_tools_shutdown": "Apagar o servidor",
+ "log_tools_upgrade": "Actualizar paquetes do sistema",
+ "log_tools_postinstall": "Postinstalación do servidor YunoHost",
+ "log_tools_migrations_migrate_forward": "Executar migracións",
+ "log_domain_main_domain": "Facer que '{}' sexa o dominio principal",
+ "log_user_permission_reset": "Restablecer permiso '{}'",
+ "log_user_permission_update": "Actualizar accesos para permiso '{}'",
+ "log_user_update": "Actualizar info da usuaria '{}'",
+ "log_user_group_update": "Actualizar grupo '{}'",
+ "log_user_group_delete": "Eliminar grupo '{}'",
+ "log_user_group_create": "Crear grupo '{}'",
+ "log_user_delete": "Eliminar usuaria '{}'",
+ "log_user_create": "Engadir usuaria '{}'",
+ "log_regen_conf": "Rexerar configuración do sistema '{}'",
+ "log_letsencrypt_cert_renew": "Anovar certificado Let's Encrypt para '{}'",
+ "log_selfsigned_cert_install": "Instalar certificado auto-asinado para o dominio '{}'",
+ "log_permission_url": "Actualizar URL relativo ao permiso '{}'",
+ "migration_0015_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo YunoHost esforzouse revisando e comprobandoa, aínda así algo podería fallar en partes do teu sistema ou as súas apps.\n\nPor tanto, é recomendable:\n- realiza unha copia de apoio de tódolos datos ou apps importantes. Máis info en https://yunohost.org/backup;\n - ten paciencia tras iniciar o proceso: dependendo da túa conexión de internet e hardware podería demorar varias horas a actualización de tódolos compoñentes.",
+ "migration_0015_system_not_fully_up_to_date": "O teu sistema non está ao día. Realiza unha actualización común antes de realizar a migración a Buster.",
+ "migration_0015_not_enough_free_space": "Queda moi pouco espazo en /var/! Deberías ter polo menos 1GB libre para realizar a migración.",
+ "migration_0015_not_stretch": "A distribución Debian actual non é Stretch!",
+ "migration_0015_yunohost_upgrade": "Iniciando a actualización do núcleo YunoHost...",
+ "migration_0015_still_on_stretch_after_main_upgrade": "Algo foi mal durante a actualiza ión principal, o sistema semella que aínda está en Debian Stretch",
+ "migration_0015_main_upgrade": "Iniciando a actualización principal...",
+ "migration_0015_patching_sources_list": "Correxindo os sources.lists...",
+ "migrations_already_ran": "Xa se realizaron estas migracións: {ids}",
+ "migration_0019_slapd_config_will_be_overwritten": "Semella que editaches manualmente a configuración slapd. Para esta migración crítica YunoHost precisa forzar a actualización da configuración slapd. Os ficheiros orixinais van ser copiados en {conf_backup_folder}.",
+ "migration_0019_add_new_attributes_in_ldap": "Engadir novos atributos para os permisos na base de datos LDAP",
+ "migration_0018_failed_to_reset_legacy_rules": "Fallou o restablecemento das regras antigas de iptables: {error}",
+ "migration_0018_failed_to_migrate_iptables_rules": "Fallou a migración das regras antigas de iptables a nftables: {error}",
+ "migration_0017_not_enough_space": "Crea espazo suficiente en {path} para executar a migración.",
+ "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 está instado, pero non postgresql 11? Algo raro debeu acontecer no teu sistema :(...",
+ "migration_0017_postgresql_96_not_installed": "PostgreSQL non está instalado no teu sistema. Nada que facer.",
+ "migration_0015_weak_certs": "Os seguintes certificados están a utilizar algoritmos de sinatura débiles e teñen que ser actualizados para ser compatibles coa seguinte versión de nginx: {certs}",
+ "migration_0015_cleaning_up": "Limpando a caché e paquetes que xa non son útiles...",
+ "migration_0015_specific_upgrade": "Iniciando a actualización dos paquetes do sistema que precisan ser actualizados de xeito independente...",
+ "migration_0015_modified_files": "Ten en conta que os seguintes ficheiros semella que foron modificados manualmente e poderían ser sobrescritos na actualización: {manually_modified_files}",
+ "migration_0015_problematic_apps_warning": "Ten en conta que se detectaron as seguintes apps que poderían ser problemáticas. Semella que non foron instaladas usando o catálogo de YunoHost, ou non están marcadas como 'funcionais'. En consecuencia, non se pode garantir que seguirán funcionando após a actualización: {problematic_apps}",
+ "diagnosis_http_localdomain": "O dominio {domain}, cun TLD .local, non é de agardar que esté exposto ao exterior da rede local.",
+ "diagnosis_dns_specialusedomain": "O dominio {domain} baséase un dominio de nivel alto e uso especial (TLD) como .local ou .test polo que non é de agardar que realmente teña rexistros DNS.",
+ "upnp_enabled": "UPnP activado",
+ "upnp_disabled": "UPnP desactivado",
+ "permission_creation_failed": "Non se creou o permiso '{permission}': {error}",
+ "permission_created": "Creado o permiso '{permission}'",
+ "permission_cannot_remove_main": "Non está permitido eliminar un permiso principal",
+ "permission_already_up_to_date": "Non se actualizou o permiso porque as solicitudes de adición/retirada xa coinciden co estado actual.",
+ "permission_already_exist": "Xa existe o permiso '{permission}'",
+ "permission_already_disallowed": "O grupo '{group}' xa ten o permiso '{permission}' desactivado",
+ "permission_already_allowed": "O grupo '{group}' xa ten o permiso '{permission}' activado",
+ "pattern_password_app": "Lamentámolo, os contrasinais non poden conter os seguintes caracteres: {forbidden_chars}",
+ "pattern_username": "Só admite caracteres alfanuméricos en minúscula e trazo baixo",
+ "pattern_port_or_range": "Debe ser un número válido de porto (entre 0-65535) ou rango de portos (ex. 100:200)",
+ "pattern_password": "Ten que ter polo menos 3 caracteres",
+ "pattern_mailbox_quota": "Ten que ser un tamaño co sufixo b/k/M/G/T ou 0 para non ter unha cota",
+ "pattern_lastname": "Ten que ser un apelido válido",
+ "pattern_firstname": "Ten que ser un nome válido",
+ "pattern_email": "Ten que ser un enderezo de email válido, sen o símbolo '+' (ex. persoa@exemplo.com)",
+ "pattern_email_forward": "Ten que ser un enderezo de email válido, está aceptado o símbolo '+' (ex. persoa+etiqueta@exemplo.com)",
+ "pattern_domain": "Ten que ser un nome de dominio válido (ex. dominiopropio.org)",
+ "pattern_backup_archive_name": "Ten que ser un nome de ficheiro válido con 30 caracteres como máximo, alfanuméricos ou só caracteres -_.",
+ "password_too_simple_4": "O contrasinal ten que ter 12 caracteres como mínimo e conter un díxito, maiúsculas, minúsculas e caracteres especiais",
+ "password_too_simple_3": "O contrasinal ten que ter 8 caracteres como mínimo e conter un díxito, maiúsculas, minúsculas e caracteres especiais",
+ "password_too_simple_2": "O contrasinal ten que ter 8 caracteres como mínimo e conter un díxito, maiúsculas e minúsculas",
+ "password_listed": "Este contrasinal está entre os máis utilizados no mundo. Por favor elixe outro que sexa máis orixinal.",
+ "packages_upgrade_failed": "Non se puideron actualizar tódolos paquetes",
+ "operation_interrupted": "Foi interrumpida manualmente a operación?",
+ "invalid_number": "Ten que ser un número",
+ "not_enough_disk_space": "Non hai espazo libre abondo en '{path}'",
+ "migrations_to_be_ran_manually": "A migración {id} ten que ser executada manualmente. Vaite a Ferramentas → Migracións na páxina webadmin, ou executa `yunohost tools migrations run`.",
+ "migrations_success_forward": "Migración {id} completada",
+ "migrations_skip_migration": "Omitindo migración {id}...",
+ "migrations_running_forward": "Realizando migración {id}...",
+ "migrations_pending_cant_rerun": "Esas migracións están pendentes, polo que non ser executadas outra vez: {ids}",
+ "migrations_not_pending_cant_skip": "Esas migracións non están pendentes, polo que non poden ser omitidas: {ids}",
+ "migrations_no_such_migration": "Non hai migración co nome '{id}'",
+ "migrations_no_migrations_to_run": "Sen migracións a executar",
+ "migrations_need_to_accept_disclaimer": "Para executar a migración {id}, tes que aceptar o seguinte aviso:\n---\n{disclaimer}\n---\nSe aceptas executar a migración, por favor volve a executar o comando coa opción '--accept-disclaimer'.",
+ "migrations_must_provide_explicit_targets": "Debes proporcionar obxectivos explícitos ao utilizar '--skip' ou '--force-rerun'",
+ "migrations_migration_has_failed": "A migración {id} non se completou, abortando. Erro: {exception}",
+ "migrations_loading_migration": "Cargando a migración {id}...",
+ "migrations_list_conflict_pending_done": "Non podes usar ao mesmo tempo '--previous' e '--done'.",
+ "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' son opcións que se exclúen unhas a outras.",
+ "migrations_failed_to_load_migration": "Non se cargou a migración {id}: {error}",
+ "migrations_dependencies_not_satisfied": "Executar estas migracións: '{dependencies_id}', antes da migración {id}.",
+ "migrations_cant_reach_migration_file": "Non se pode acceder aos ficheiros de migración na ruta '%s'",
+ "regenconf_file_manually_removed": "O ficheiro de configuración '{conf}' foi eliminado manualmente e non será creado",
+ "regenconf_file_manually_modified": "O ficheiro de configuración '{conf}' foi modificado manualmente e non vai ser actualizado",
+ "regenconf_file_kept_back": "Era de agardar que o ficheiro de configuración '{conf}' fose eliminado por regen-conf (categoría {category}) mais foi mantido.",
+ "regenconf_file_copy_failed": "Non se puido copiar o novo ficheiro de configuración '{new}' a '{conf}'",
+ "regenconf_file_backed_up": "Ficheiro de configuración '{conf}' copiado a '{backup}'",
+ "postinstall_low_rootfsspace": "O sistema de ficheiros raiz ten un espazo total menor de 10GB, que é pouco! Probablemente vas quedar sen espazo moi pronto! É recomendable ter polo menos 16GB para o sistema raíz. Se queres instalar YunoHost obviando este aviso, volve a executar a postinstalación con --force-diskspace",
+ "port_already_opened": "O porto {port} xa está aberto para conexións {ip_version}",
+ "port_already_closed": "O porto {port} xa está pechado para conexións {ip_version}",
+ "permission_require_account": "O permiso {permission} só ten sentido para usuarias cunha conta, e por tanto non pode concederse a visitantes.",
+ "permission_protected": "O permiso {permission} está protexido. Non podes engadir ou eliminar o grupo visitantes a/de este permiso.",
+ "permission_updated": "Permiso '{permission}' actualizado",
+ "permission_update_failed": "Non se actualizou o permiso '{permission}': {error}",
+ "permission_not_found": "Non se atopa o permiso '{permission}'",
+ "permission_deletion_failed": "Non se puido eliminar o permiso '{permission}': {error}",
+ "permission_deleted": "O permiso '{permission}' foi eliminado",
+ "permission_cant_add_to_all_users": "O permiso {permission} non pode ser concecido a tódalas usuarias.",
+ "permission_currently_allowed_for_all_users": "Este permiso está concedido actualmente a tódalas usuarias ademáis de a outros grupos. Probablemente queiras ben eliminar o permiso 'all_users' ou ben eliminar os outros grupos que teñen permiso.",
+ "restore_failed": "Non se puido restablecer o sistema",
+ "restore_extracting": "Extraendo os ficheiros necesarios desde o arquivo...",
+ "restore_confirm_yunohost_installed": "Tes a certeza de querer restablecer un sistema xa instalado? [{answers}]",
+ "restore_complete": "Restablecemento completado",
+ "restore_cleaning_failed": "Non se puido despexar o directorio temporal de restablecemento",
+ "restore_backup_too_old": "Este arquivo de apoio non pode ser restaurado porque procede dunha versión YunoHost demasiado antiga.",
+ "restore_already_installed_apps": "As seguintes apps non se poden restablecer porque xa están instaladas: {apps}",
+ "restore_already_installed_app": "Unha app con ID '{app}' xa está instalada",
+ "regex_with_only_domain": "Agora xa non podes usar un regex para o dominio, só para ruta",
+ "regex_incompatible_with_tile": "/!\\ Empacadoras! O permiso '{permission}' agora ten show_tile establecido como 'true' polo que non podes definir o regex URL como URL principal",
+ "regenconf_need_to_explicitly_specify_ssh": "A configuración ssh foi modificada manualmente, pero tes que indicar explícitamente a categoría 'ssh' con --force para realmente aplicar os cambios.",
+ "regenconf_pending_applying": "Aplicando a configuración pendente para categoría '{category}'...",
+ "regenconf_failed": "Non se rexenerou a configuración para a categoría(s): {categories}",
+ "regenconf_dry_pending_applying": "Comprobando as configuracións pendentes que deberían aplicarse á categoría '{category}'...",
+ "regenconf_would_be_updated": "A configuración debería ser actualizada para a categoría '{category}'",
+ "regenconf_updated": "Configuración actualizada para '{category}'",
+ "regenconf_up_to_date": "A configuración xa está ao día para a categoría '{category}'",
+ "regenconf_now_managed_by_yunohost": "O ficheiro de configuración '{conf}' agora está xestionado por YunoHost (categoría {category}).",
+ "regenconf_file_updated": "Actualizado o ficheiro de configuración '{conf}'",
+ "regenconf_file_removed": "Eliminado o ficheiro de configuración '{conf}'",
+ "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 '{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)",
+ "service_description_slapd": "Almacena usuarias, dominios e info relacionada",
+ "service_description_rspamd": "Filtra spam e outras características relacionadas co email",
+ "service_description_redis-server": "Unha base de datos especial utilizada para o acceso rápido a datos, cola de tarefas e comunicación entre programas",
+ "service_description_postfix": "Utilizado para enviar e recibir emails",
+ "service_description_php7.3-fpm": "Executa aplicacións escritas en PHP con NGINX",
+ "service_description_nginx": "Serve ou proporciona acceso a tódolos sitios web hospedados no teu servidor",
+ "service_description_mysql": "Almacena datos da app (base de datos SQL)",
+ "service_description_metronome": "Xestiona as contas de mensaxería instantánea XMPP",
+ "service_description_fail2ban": "Protexe contra ataques de forza bruta e outro tipo de ataques desde internet",
+ "service_description_dovecot": "Permite aos clientes de email acceder/obter o correo (vía IMAP e POP3)",
+ "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 '{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}'",
+ "server_reboot_confirm": "Queres reiniciar o servidor inmediatamente? [{answers}]",
+ "server_reboot": "Vaise reiniciar o servidor",
+ "server_shutdown_confirm": "Queres apagar o servidor inmediatamente? [{answers}]",
+ "server_shutdown": "Vaise apagar o servidor",
+ "root_password_replaced_by_admin_password": "O contrasinal root foi substituído polo teu contrasinal de administración.",
+ "root_password_desynchronized": "Mudou o contrasinal de administración, pero YunoHost non puido transferir este cambio ao contrasinal root!",
+ "restore_system_part_failed": "Non se restableceu a parte do sistema '{part}'",
+ "restore_running_hooks": "Executando os ganchos do restablecemento...",
+ "restore_running_app_script": "Restablecendo a app '{app}'...",
+ "restore_removing_tmp_dir_failed": "Non se puido eliminar o directorio temporal antigo",
+ "restore_nothings_done": "Nada foi restablecido",
+ "restore_not_enough_disk_space": "Non hai espazo abondo (espazo: {free_space.d} B, espazo necesario: {needed_space} B, marxe de seguridade: {margin} B)",
+ "restore_may_be_not_enough_disk_space": "O teu sistema semella que non ten espazo abondo (libre: {free_space} B, espazo necesario: {needed_space} B, marxe de seguridade {margin} B)",
+ "restore_hook_unavailable": "O script de restablecemento para '{part}' non está dispoñible no teu sistema nin no arquivo",
+ "invalid_password": "Contrasinal non válido",
+ "ldap_server_is_down_restart_it": "O servidor LDAP está caído, intenta reinicialo...",
+ "ldap_server_down": "Non se chegou ao servidor LDAP",
+ "global_settings_setting_security_experimental_enabled": "Activar características de seguridade experimentais (non actives isto se non sabes o que estás a facer!)",
+ "yunohost_postinstall_end_tip": "Post-install completada! Para rematar a configuración considera:\n- engadir unha primeira usuaria na sección 'Usuarias' na webadmin (ou 'yunohost user create ' na liña de comandos);\n- diagnosticar potenciais problemas na sección 'Diagnóstico' na webadmin (ou 'yunohost diagnosis run' na liña de comandos);\n- ler 'Rematando a configuración' e 'Coñece YunoHost' na documentación da administración: https://yunohost.org/admindoc.",
+ "yunohost_not_installed": "YunoHost non está instalado correctamente. Executa 'yunohost tools postinstall'",
+ "yunohost_installing": "Instalando YunoHost...",
+ "yunohost_configured": "YunoHost está configurado",
+ "yunohost_already_installed": "YunoHost xa está instalado",
+ "user_updated": "Cambiada a info da usuaria",
+ "user_update_failed": "Non se actualizou usuaria {user}: {error}",
+ "user_unknown": "Usuaria descoñecida: {user}",
+ "user_home_creation_failed": "Non se puido crear cartafol home '{home}' para a usuaria",
+ "user_deletion_failed": "Non se puido eliminar a usuaria {user}: {error}",
+ "user_deleted": "Usuaria eliminada",
+ "user_creation_failed": "Non se puido crear a usuaria {user}: {error}",
+ "user_created": "Usuaria creada",
+ "user_already_exists": "A usuaria '{user}' xa existe",
+ "upnp_port_open_failed": "Non se puido abrir porto a través de UPnP",
+ "upnp_dev_not_found": "Non se atopa dispositivo UPnP",
+ "upgrading_packages": "Actualizando paquetes...",
+ "upgrade_complete": "Actualización completa",
+ "updating_apt_cache": "Obtendo actualizacións dispoñibles para os paquetes do sistema...",
+ "update_apt_cache_warning": "Algo fallou ao actualizar a caché de APT (xestor de paquetes Debian). Aquí tes un volcado de sources.list, que podería axudar a identificar liñas problemáticas:\n{sourceslist}",
+ "update_apt_cache_failed": "Non se puido actualizar a caché de APT (xestor de paquetes de Debian). Aquí tes un volcado do sources.list, que podería axudarche a identificar liñas incorrectas:\n{sourceslist}",
+ "unrestore_app": "{app} non vai ser restablecida",
+ "unlimit": "Sen cota",
+ "unknown_main_domain_path": "Dominio ou ruta descoñecida '{app}'. Tes que indicar un dominio e ruta para poder especificar un URL para o permiso.",
+ "unexpected_error": "Aconteceu un fallo non agardado: {error}",
+ "unbackup_app": "{app} non vai ser gardada",
+ "tools_upgrade_special_packages_completed": "Completada a actualización dos paquetes YunoHost.\nPreme [Enter] para recuperar a liña de comandos",
+ "tools_upgrade_special_packages_explanation": "A actualización especial continuará en segundo plano. Non inicies outras tarefas no servidor nos seguintes ~10 minutos (depende do hardware). Após isto, podes volver a conectar na webadmin. O rexistro da actualización estará dispoñible en Ferramentas → Rexistro (na webadmin) ou con 'yunohost log list' (na liña de comandos).",
+ "tools_upgrade_special_packages": "Actualizando paquetes 'special' (yunohost-related)...",
+ "tools_upgrade_regular_packages_failed": "Non se actualizaron os paquetes: {packages_list}",
+ "tools_upgrade_regular_packages": "Actualizando os paquetes 'regular' (non-yunohost-related)...",
+ "tools_upgrade_cant_unhold_critical_packages": "Non se desbloquearon os paquetes críticos...",
+ "tools_upgrade_cant_hold_critical_packages": "Non se puideron bloquear os paquetes críticos...",
+ "tools_upgrade_cant_both": "Non se pode actualizar o sistema e as apps ao mesmo tempo",
+ "tools_upgrade_at_least_one": "Por favor indica 'apps', ou 'system'",
+ "this_action_broke_dpkg": "Esta acción rachou dpkg/APT (xestores de paquetes do sistema)... Podes intentar resolver o problema conectando a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.",
+ "system_username_exists": "Xa existe este nome de usuaria na lista de usuarias do sistema",
+ "system_upgraded": "Sistema actualizado",
+ "ssowat_conf_updated": "Actualizada a configuración SSOwat",
+ "ssowat_conf_generated": "Rexenerada a configuración para SSOwat",
+ "show_tile_cant_be_enabled_for_regex": "Non podes activar 'show_tile' neste intre, porque o URL para o permiso '{permission}' é un regex",
+ "show_tile_cant_be_enabled_for_url_not_defined": "Non podes activar 'show_tile' neste intre, primeiro tes que definir un URL para o permiso '{permission}'",
+ "service_unknown": "Servizo descoñecido '{service}'",
+ "service_stopped": "Detívose o servizo '{service}'",
+ "service_stop_failed": "Non se puido deter o servizo '{service}'\n\nRexistros recentes do servizo: {logs}",
+ "service_started": "Iniciado o servizo '{service}'",
+ "service_start_failed": "Non se puido iniciar o servizo '{service}'\n\nRexistros recentes do servizo: {logs}",
+ "service_reloaded_or_restarted": "O servizo '{service}' foi recargado ou reiniciado",
+ "service_reload_or_restart_failed": "Non se recargou ou reiniciou o servizo '{service}'\n\nRexistros recentes do servizo: {logs}",
+ "service_restarted": "Reiniciado o servizo '{service}'",
+ "service_restart_failed": "Non se reiniciou o servizo '{service}'\n\nRexistros recentes do servizo: {logs}",
+ "service_reloaded": "Recargado o servizo '{service}'",
+ "service_reload_failed": "Non se recargou o servizo '{service}'\n\nRexistros recentes do servizo: {logs}",
+ "service_removed": "Eliminado o servizo '{service}'",
+ "service_remove_failed": "Non se eliminou o servizo '{service}'",
+ "service_regen_conf_is_deprecated": "'yunohost service regen-conf' xa non se utiliza! Executa 'yunohost tools regen-conf' no seu lugar.",
+ "service_enabled": "O servizo '{service}' vai ser iniciado automáticamente no inicio do sistema.",
+ "diagnosis_apps_allgood": "Tódalas apps instaladas respectan as prácticas básicas de empaquetado",
+ "diagnosis_apps_bad_quality": "Esta aplicación está actualmente marcada como estragada no catálogo de aplicacións de YunoHost. Podería ser un problema temporal mentras as mantedoras intentan arranxar o problema. Ata ese momento a actualización desta app está desactivada.",
+ "global_settings_setting_security_nginx_redirect_to_https": "Redirixir peticións HTTP a HTTPs por defecto (NON DESACTIVAR ISTO a non ser que realmente saibas o que fas!)",
+ "log_user_import": "Importar usuarias",
+ "user_import_failed": "A operación de importación de usuarias fracasou",
+ "user_import_missing_columns": "Faltan as seguintes columnas: {columns}",
+ "user_import_nothing_to_do": "Ningunha usuaria precisa ser importada",
+ "user_import_partial_failed": "A operación de importación de usuarias fallou parcialmente",
+ "diagnosis_apps_deprecated_practices": "A versión instalada desta app aínda utiliza algunha das antigas prácticas de empaquetado xa abandonadas. Deberías considerar actualizala.",
+ "diagnosis_apps_outdated_ynh_requirement": "A versión instalada desta app só require yunohost >= 2.x, que normalmente indica que non está ao día coas prácticas recomendadas de empaquetado e asistentes. Deberías considerar actualizala.",
+ "user_import_success": "Usuarias importadas correctamente",
+ "diagnosis_high_number_auth_failures": "Hai un alto número sospeitoso de intentos fallidos de autenticación. Deberías comprobar que fail2ban está a executarse e que está correctamente configurado, ou utiliza un porto personalizado para SSH tal como se explica en https://yunohost.org/security.",
+ "user_import_bad_file": "O ficheiro CSV non ten o formato correcto e será ignorado para evitar unha potencial perda de datos",
+ "user_import_bad_line": "Liña incorrecta {line}: {details}",
+ "diagnosis_description_apps": "Aplicacións",
+ "diagnosis_apps_broken": "Actualmente esta aplicación está marcada como estragada no catálogo de aplicacións de YunoHost. Podería tratarse dun problema temporal mentras as mantedoras intentan arraxala. Entanto así a actualización da app está desactivada.",
+ "diagnosis_apps_issue": "Atopouse un problema na app {app}",
+ "diagnosis_apps_not_in_app_catalog": "Esta aplicación non está no catálgo de aplicacións de YunoHost. Se estivo no pasado e foi eliminada, deberías considerar desinstalala porque non recibirá actualizacións, e podería comprometer a integridade e seguridade do teu sistema.",
+ "app_argument_password_help_optional": "Escribe un espazo para limpar o contrasinal",
+ "config_validate_date": "Debe ser unha data válida co formato YYYY-MM-DD",
+ "config_validate_email": "Debe ser un email válido",
+ "config_validate_time": "Debe ser unha hora válida tal que HH:MM",
+ "config_validate_url": "Debe ser un URL válido",
+ "danger": "Perigo:",
+ "app_argument_password_help_keep": "Preme Enter para manter o valor actual",
+ "app_config_unable_to_read": "Fallou a lectura dos valores de configuración.",
+ "config_apply_failed": "Fallou a aplicación da nova configuración: {error}",
+ "config_forbidden_keyword": "O palabra chave '{keyword}' está reservada, non podes crear ou usar un panel de configuración cunha pregunta con este id.",
+ "config_no_panel": "Non se atopa panel configurado.",
+ "config_unknown_filter_key": "A chave do filtro '{filter_key}' non é correcta.",
+ "config_validate_color": "Debe ser un valor RGB hexadecimal válido",
+ "invalid_number_min": "Ten que ser maior que {min}",
+ "log_app_config_set": "Aplicar a configuración á app '{}'",
+ "app_config_unable_to_apply": "Fallou a aplicación dos valores de configuración.",
+ "config_cant_set_value_on_section": "Non podes establecer un valor único na sección completa de configuración.",
+ "config_version_not_supported": "A versión do panel de configuración '{version}' non está soportada.",
+ "file_extension_not_accepted": "Rexeitouse o ficheiro '{path}' porque a súa extensión non está entre as aceptadas: {accept}",
+ "invalid_number_max": "Ten que ser menor de {max}",
+ "service_not_reloading_because_conf_broken": "Non se recargou/reiniciou o servizo '{name}' porque a súa configuración está estragada: {errors}",
+ "diagnosis_http_special_use_tld": "O dominio {domain} baséase nun dominio de alto-nivel (TLD) especial como .local ou .test e por isto non é de agardar que esté exposto fóra da rede local.",
+ "domain_dns_conf_special_use_tld": "Este dominio baséase nun dominio de alto-nivel (TLD) de uso especial como .local ou .test e por isto non é de agardar que teña rexistros DNS asociados.",
+ "domain_dns_registrar_managed_in_parent_domain": "Este dominio é un subdominio de {parent_domain_link}. A configuración DNS debe xestionarse no panel de configuración de {parent_domain}'s.",
+ "domain_dns_registrar_not_supported": "YunoHost non é quen de detectar a rexistradora que xestiona o dominio. Debes configurar manualmente os seus rexistros DNS seguindo a documentación en https://yunohost.org/dns.",
+ "domain_dns_registrar_experimental": "Ata o momento, a interface coa API de **{registrar}** aínda non foi comprobada e revisada pola comunidade YunoHost. O soporte é **moi experimental** - ten coidado!",
+ "domain_dns_push_failed_to_list": "Non se pode mostrar a lista actual de rexistros na API da rexistradora: {error}",
+ "domain_dns_push_already_up_to_date": "Rexistros ao día, nada que facer.",
+ "domain_dns_pushing": "Enviando rexistros DNS...",
+ "domain_dns_push_record_failed": "Fallou {action} do rexistro {type}/{name}: {error}",
+ "domain_dns_push_success": "Rexistros DNS actualizados!",
+ "domain_dns_push_failed": "Fallou completamente a actualización dos rexistros DNS.",
+ "domain_config_features_disclaimer": "Ata o momento, activar/desactivar as funcións de email ou XMPP só ten impacto na configuración automática da configuración DNS, non na configuración do sistema!",
+ "domain_config_mail_in": "Emails entrantes",
+ "domain_config_mail_out": "Emails saíntes",
+ "domain_config_xmpp": "Mensaxería instantánea (XMPP)",
+ "domain_config_auth_secret": "Segreda de autenticación",
+ "domain_config_api_protocol": "Protocolo API",
+ "domain_config_auth_application_key": "Chave da aplicación",
+ "domain_config_auth_application_secret": "Chave segreda da aplicación",
+ "domain_config_auth_consumer_key": "Chave consumidora",
+ "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'",
+ "other_available_options": "... e outras {n} opcións dispoñibles non mostradas",
+ "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')",
+ "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )",
+ "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.",
+ "domain_config_auth_token": "Token de autenticación",
+ "domain_config_auth_key": "Chave de autenticación",
+ "domain_config_auth_entrypoint": "Punto de entrada da API",
+ "domain_dns_push_failed_to_authenticate": "Fallou a autenticación na API da rexistradora do dominio '{domain}'. Comprobaches que sexan as credenciais correctas? (Erro: {error})",
+ "domain_registrar_is_not_configured": "A rexistradora non aínda non está configurada para o dominio {domain}.",
+ "domain_dns_push_not_applicable": "A función de rexistro DNS automático non é aplicable ao dominio {domain}. Debes configurar manualmente os teus rexistros DNS seguindo a documentación de https://yunohost.org/dns_config.",
+ "domain_dns_push_managed_in_parent_domain": "A función de rexistro DNS automático está xestionada polo dominio nai {parent_domain}.",
+ "ldap_attribute_already_exists": "Xa existe o atributo LDAP '{attribute}' con valor '{value}'",
+ "log_domain_config_set": "Actualizar configuración para o dominio '{}'"
+}
diff --git a/locales/hi.json b/locales/hi.json
index 609464c8f..5f521b1dc 100644
--- a/locales/hi.json
+++ b/locales/hi.json
@@ -1,51 +1,49 @@
{
- "action_invalid": "अवैध कार्रवाई '{action:s}'",
+ "action_invalid": "अवैध कार्रवाई '{action}'",
"admin_password": "व्यवस्थापक पासवर्ड",
"admin_password_change_failed": "पासवर्ड बदलने में असमर्थ",
"admin_password_changed": "व्यवस्थापक पासवर्ड बदल दिया गया है",
- "app_already_installed": "'{app:s}' पहले से ही इंस्टाल्ड है",
- "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name:s}' , तर्क इन विकल्पों में से होने चाहिए {choices:s}",
- "app_argument_invalid": "तर्क के लिए अमान्य मान '{name:s}': {error:s}",
- "app_argument_required": "तर्क '{name:s}' की आवश्यकता है",
+ "app_already_installed": "'{app}' पहले से ही इंस्टाल्ड है",
+ "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name}' , तर्क इन विकल्पों में से होने चाहिए {choices}",
+ "app_argument_invalid": "तर्क के लिए अमान्य मान '{name}': {error}",
+ "app_argument_required": "तर्क '{name}' की आवश्यकता है",
"app_extraction_failed": "इन्सटाल्ड फ़ाइलों को निकालने में असमर्थ",
"app_id_invalid": "अवैध एप्लिकेशन id",
"app_install_files_invalid": "फाइलों की अमान्य स्थापना",
"app_manifest_invalid": "एप्लीकेशन का मैनिफेस्ट अमान्य",
- "app_not_correctly_installed": "{app:s} ठीक ढंग से इनस्टॉल नहीं हुई",
- "app_not_installed": "{app:s} इनस्टॉल नहीं हुई",
- "app_not_properly_removed": "{app:s} ठीक ढंग से नहीं अनइन्सटॉल की गई",
- "app_removed": "{app:s} को अनइन्सटॉल कर दिया गया",
+ "app_not_correctly_installed": "{app} ठीक ढंग से इनस्टॉल नहीं हुई",
+ "app_not_installed": "{app} इनस्टॉल नहीं हुई",
+ "app_not_properly_removed": "{app} ठीक ढंग से नहीं अनइन्सटॉल की गई",
+ "app_removed": "{app} को अनइन्सटॉल कर दिया गया",
"app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है ....",
"app_requirements_unmeet": "आवश्यकताए पूरी नहीं हो सकी, पैकेज {pkgname}({version})यह होना चाहिए {spec}",
"app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ",
"app_unknown": "अनजान एप्लीकेशन",
"app_unsupported_remote_type": "एप्लीकेशन के लिए उन्सुपपोर्टेड रिमोट टाइप इस्तेमाल किया गया",
- "app_upgrade_failed": "{app:s} अपडेट करने में असमर्थ",
- "app_upgraded": "{app:s} अपडेट हो गयी हैं",
- "ask_email": "ईमेल का पता",
+ "app_upgrade_failed": "{app} अपडेट करने में असमर्थ",
+ "app_upgraded": "{app} अपडेट हो गयी हैं",
"ask_firstname": "नाम",
"ask_lastname": "अंतिम नाम",
"ask_main_domain": "मुख्य डोमेन",
"ask_new_admin_password": "नया व्यवस्थापक पासवर्ड",
"ask_password": "पासवर्ड",
- "backup_app_failed": "एप्लीकेशन का बैकअप करने में असमर्थ '{app:s}'",
- "backup_archive_app_not_found": "'{app:s}' बैकअप आरचिव में नहीं मिला",
+ "backup_app_failed": "एप्लीकेशन का बैकअप करने में असमर्थ '{app}'",
+ "backup_archive_app_not_found": "'{app}' बैकअप आरचिव में नहीं मिला",
"backup_archive_name_exists": "इस बैकअप आरचिव का नाम पहले से ही मौजूद है",
- "backup_archive_name_unknown": "'{name:s}' इस नाम की लोकल बैकअप आरचिव मौजूद नहीं",
+ "backup_archive_name_unknown": "'{name}' इस नाम की लोकल बैकअप आरचिव मौजूद नहीं",
"backup_archive_open_failed": "बैकअप आरचिव को खोलने में असमर्थ",
"backup_cleaning_failed": "टेम्पोरेरी बैकअप डायरेक्टरी को उड़ने में असमर्थ",
"backup_created": "बैकअप सफलतापूर्वक किया गया",
"backup_creation_failed": "बैकअप बनाने में विफल",
- "backup_delete_error": "'{path:s}' डिलीट करने में असमर्थ",
+ "backup_delete_error": "'{path}' डिलीट करने में असमर्थ",
"backup_deleted": "इस बैकअप को डिलीट दिया गया है",
- "backup_hook_unknown": "'{hook:s}' यह बैकअप हुक नहीं मिला",
- "backup_invalid_archive": "अवैध बैकअप आरचिव",
+ "backup_hook_unknown": "'{hook}' यह बैकअप हुक नहीं मिला",
"backup_nothings_done": "सेव करने के लिए कुछ नहीं",
"backup_output_directory_forbidden": "निषिद्ध आउटपुट डायरेक्टरी। निम्न दिए गए डायरेक्टरी में बैकअप नहीं बन सकता /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var और /home/yunohost.backup/archives के सब-फोल्डर।",
"backup_output_directory_not_empty": "आउटपुट डायरेक्टरी खाली नहीं है",
"backup_output_directory_required": "बैकअप करने के लिए आउट पुट डायरेक्टरी की आवश्यकता है",
"backup_running_hooks": "बैकअप हुक्स चल रहे है...",
- "custom_app_url_required": "आप को अपनी कस्टम एप्लिकेशन '{app:s}' को अपग्रेड करने के लिए यूआरएल(URL) देने की आवश्यकता है",
+ "custom_app_url_required": "आप को अपनी कस्टम एप्लिकेशन '{app}' को अपग्रेड करने के लिए यूआरएल(URL) देने की आवश्यकता है",
"domain_cert_gen_failed": "सर्टिफिकेट उत्पन करने में असमर्थ",
"domain_created": "डोमेन बनाया गया",
"domain_creation_failed": "डोमेन बनाने में असमर्थ",
diff --git a/locales/hu.json b/locales/hu.json
index 3ba14286f..9c482a370 100644
--- a/locales/hu.json
+++ b/locales/hu.json
@@ -1,14 +1,14 @@
{
"aborting": "Megszakítás.",
- "action_invalid": "Érvénytelen művelet '{action:s}'",
+ "action_invalid": "Érvénytelen művelet '{action}'",
"admin_password": "Adminisztrátori jelszó",
"admin_password_change_failed": "Nem lehet a jelszót megváltoztatni",
"admin_password_changed": "Az adminisztrátori jelszó megváltozott",
- "app_already_installed": "{app:s} már telepítve van",
+ "app_already_installed": "{app} már telepítve van",
"app_already_installed_cant_change_url": "Ez az app már telepítve van. Ezzel a funkcióval az url nem változtatható. Javaslat 'app url változtatás' ha lehetséges.",
- "app_already_up_to_date": "{app:s} napra kész",
- "app_argument_choice_invalid": "{name:s} érvénytelen választás, csak egyike lehet {choices:s} közül",
- "app_argument_invalid": "'{name:s}' hibás paraméter érték :{error:s}",
- "app_argument_required": "Parameter '{name:s}' kötelező",
+ "app_already_up_to_date": "{app} napra kész",
+ "app_argument_choice_invalid": "{name} érvénytelen választás, csak egyike lehet {choices} közül",
+ "app_argument_invalid": "'{name}' hibás paraméter érték :{error}",
+ "app_argument_required": "Parameter '{name}' kötelező",
"password_too_simple_1": "A jelszónak legalább 8 karakter hosszúnak kell lennie"
}
\ No newline at end of file
diff --git a/locales/id.json b/locales/id.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/locales/id.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/locales/it.json b/locales/it.json
index 29f1db1e9..1332712ef 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -1,28 +1,25 @@
{
- "app_already_installed": "{app:s} è già installata",
+ "app_already_installed": "{app} è già installata",
"app_extraction_failed": "Impossibile estrarre i file di installazione",
- "app_not_installed": "Impossibile trovare l'applicazione {app:s} nell'elenco delle applicazioni installate: {all_apps}",
+ "app_not_installed": "Impossibile trovare l'applicazione {app} nell'elenco delle applicazioni installate: {all_apps}",
"app_unknown": "Applicazione sconosciuta",
- "ask_email": "Indirizzo email",
"ask_password": "Password",
"backup_archive_name_exists": "Il nome dell'archivio del backup è già esistente.",
"backup_created": "Backup completo",
- "backup_invalid_archive": "Archivio di backup non valido",
"backup_output_directory_not_empty": "Dovresti scegliere una cartella di output vuota",
"domain_created": "Dominio creato",
"domain_exists": "Il dominio esiste già",
- "ldap_initialized": "LDAP inizializzato",
"pattern_email": "L'indirizzo email deve essere valido, senza simboli '+' (es. tizio@dominio.com)",
"pattern_mailbox_quota": "La dimensione deve avere un suffisso b/k/M/G/T o 0 per disattivare la quota",
- "port_already_opened": "La porta {port:d} è già aperta per {ip_version:s} connessioni",
- "service_add_failed": "Impossibile aggiungere il servizio '{service:s}'",
- "service_cmd_exec_failed": "Impossibile eseguire il comando '{command:s}'",
- "service_disabled": "Il servizio '{service:s}' non partirà più al boot di sistema.",
- "service_remove_failed": "Impossibile rimuovere il servizio '{service:s}'",
- "service_removed": "Servizio '{service:s}' rimosso",
- "service_stop_failed": "Impossibile fermare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
+ "port_already_opened": "La porta {port} è già aperta per {ip_version} connessioni",
+ "service_add_failed": "Impossibile aggiungere il servizio '{service}'",
+ "service_cmd_exec_failed": "Impossibile eseguire il comando '{command}'",
+ "service_disabled": "Il servizio '{service}' non partirà più al boot di sistema.",
+ "service_remove_failed": "Impossibile rimuovere il servizio '{service}'",
+ "service_removed": "Servizio '{service}' rimosso",
+ "service_stop_failed": "Impossibile fermare il servizio '{service}'\n\nRegistri di servizio recenti:{logs}",
"system_username_exists": "Il nome utente esiste già negli utenti del sistema",
- "unrestore_app": "{app:s} non verrà ripristinata",
+ "unrestore_app": "{app} non verrà ripristinata",
"upgrading_packages": "Aggiornamento dei pacchetti...",
"user_deleted": "Utente cancellato",
"admin_password": "Password dell'amministrazione",
@@ -30,39 +27,39 @@
"admin_password_changed": "La password d'amministrazione è stata cambiata",
"app_install_files_invalid": "Questi file non possono essere installati",
"app_manifest_invalid": "C'è qualcosa di scorretto nel manifesto dell'applicazione: {error}",
- "app_not_correctly_installed": "{app:s} sembra di non essere installata correttamente",
- "app_not_properly_removed": "{app:s} non è stata correttamente rimossa",
- "action_invalid": "L'azione '{action:s}' non è valida",
- "app_removed": "{app:s} rimossa",
+ "app_not_correctly_installed": "{app} sembra di non essere installata correttamente",
+ "app_not_properly_removed": "{app} non è stata correttamente rimossa",
+ "action_invalid": "L'azione '{action}' non è valida",
+ "app_removed": "{app} disinstallata",
"app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l'URL è corretto?",
- "app_upgrade_failed": "Impossibile aggiornare {app:s}: {error}",
- "app_upgraded": "{app:s} aggiornata",
+ "app_upgrade_failed": "Impossibile aggiornare {app}: {error}",
+ "app_upgraded": "{app} aggiornata",
"app_requirements_checking": "Controllo i pacchetti richiesti per {app}...",
"app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}",
"ask_firstname": "Nome",
"ask_lastname": "Cognome",
"ask_main_domain": "Dominio principale",
"ask_new_admin_password": "Nuova password dell'amministrazione",
- "backup_app_failed": "Non è possibile fare il backup {app:s}",
- "backup_archive_app_not_found": "{app:s} non è stata trovata nel archivio di backup",
- "app_argument_choice_invalid": "Usa una delle seguenti scelte '{choices:s}' per il parametro '{name:s}'",
- "app_argument_invalid": "Scegli un valore valido per il parametro '{name:s}': {error:s}",
- "app_argument_required": "L'argomento '{name:s}' è requisito",
+ "backup_app_failed": "Non è possibile fare il backup {app}",
+ "backup_archive_app_not_found": "{app} non è stata trovata nel archivio di backup",
+ "app_argument_choice_invalid": "Usa una delle seguenti scelte '{choices}' per il parametro '{name}' invece di '{value}'",
+ "app_argument_invalid": "Scegli un valore valido per il parametro '{name}': {error}",
+ "app_argument_required": "L'argomento '{name}' è requisito",
"app_id_invalid": "Identificativo dell'applicazione non valido",
"app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato",
- "backup_archive_broken_link": "Non è possibile accedere all'archivio di backup (link rotto verso {path:s})",
- "backup_archive_name_unknown": "Archivio di backup locale chiamato '{name:s}' sconosciuto",
+ "backup_archive_broken_link": "Non è possibile accedere all'archivio di backup (link rotto verso {path})",
+ "backup_archive_name_unknown": "Archivio di backup locale chiamato '{name}' sconosciuto",
"backup_archive_open_failed": "Impossibile aprire l'archivio di backup",
"backup_cleaning_failed": "Non è possibile pulire la directory temporanea di backup",
"backup_creation_failed": "Impossibile creare l'archivio di backup",
- "backup_delete_error": "Impossibile cancellare '{path:s}'",
+ "backup_delete_error": "Impossibile cancellare '{path}'",
"backup_deleted": "Backup cancellato",
- "backup_hook_unknown": "Hook di backup '{hook:s}' sconosciuto",
+ "backup_hook_unknown": "Hook di backup '{hook}' sconosciuto",
"backup_nothings_done": "Niente da salvare",
"backup_output_directory_forbidden": "Scegli una diversa directory di output. I backup non possono esser creati nelle sotto-cartelle /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives",
"backup_output_directory_required": "Devi fornire una directory di output per il backup",
"backup_running_hooks": "Esecuzione degli hook di backup…",
- "custom_app_url_required": "Devi fornire un URL per essere in grado di aggiornare l'applicazione personalizzata {app:s}",
+ "custom_app_url_required": "Devi fornire un URL per essere in grado di aggiornare l'applicazione personalizzata {app}",
"domain_creation_failed": "Impossibile creare il dominio {domain}: {error}",
"domain_deleted": "Dominio cancellato",
"domain_deletion_failed": "Impossibile cancellare il dominio {domain}: {error}",
@@ -70,45 +67,35 @@
"domain_dyndns_root_unknown": "Dominio radice DynDNS sconosciuto",
"domain_hostname_failed": "Impossibile impostare il nuovo hostname. Potrebbe causare problemi in futuro (o anche no).",
"domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio:\n{apps}\n\nDisinstallale eseguendo 'yunohost app remove app_id' o spostale in un altro dominio eseguendo 'yunohost app change-url app_id' prima di procedere alla cancellazione del dominio",
- "domain_unknown": "Dominio sconosciuto",
"done": "Terminato",
"domains_available": "Domini disponibili:",
"downloading": "Scaricamento…",
- "dyndns_cron_installed": "Cronjob DynDNS creato",
- "dyndns_cron_remove_failed": "Impossibile rimuovere il cronjob DynDNS perchè: {error}",
- "dyndns_cron_removed": "Cronjob DynDNS rimosso",
"dyndns_ip_update_failed": "Impossibile aggiornare l'indirizzo IP in DynDNS",
"dyndns_ip_updated": "Il tuo indirizzo IP è stato aggiornato su DynDNS",
"dyndns_key_generating": "Generando la chiave DNS... Potrebbe richiedere del tempo.",
"dyndns_key_not_found": "La chiave DNS non è stata trovata per il dominio",
"dyndns_no_domain_registered": "Nessuno dominio registrato con DynDNS",
"dyndns_registered": "Dominio DynDNS registrato",
- "dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error:s}",
- "dyndns_unavailable": "Il dominio {domain:s} non disponibile.",
- "executing_command": "Esecuzione del comando '{command:s}'…",
- "executing_script": "Esecuzione dello script '{script:s}'…",
+ "dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error}",
+ "dyndns_unavailable": "Il dominio {domain} non disponibile.",
"extracting": "Estrazione...",
- "field_invalid": "Campo '{:s}' non valido",
+ "field_invalid": "Campo '{}' non valido",
"firewall_reload_failed": "Impossibile ricaricare il firewall",
"firewall_reloaded": "Firewall ricaricato",
"firewall_rules_cmd_failed": "Alcune regole del firewall sono fallite. Per ulteriori informazioni, vedi il registro.",
- "hook_exec_failed": "Impossibile eseguire lo script: {path:s}",
- "hook_exec_not_terminated": "Los script non è stato eseguito correttamente: {path:s}",
- "hook_name_unknown": "Nome di hook '{name:s}' sconosciuto",
+ "hook_exec_failed": "Impossibile eseguire lo script: {path}",
+ "hook_exec_not_terminated": "Los script non è stato eseguito correttamente: {path}",
+ "hook_name_unknown": "Nome di hook '{name}' sconosciuto",
"installation_complete": "Installazione completata",
- "installation_failed": "Qualcosa è andato storto durante l'installazione",
"ip6tables_unavailable": "Non puoi giocare con ip6tables qui. O sei in un container o il tuo kernel non lo supporta",
"iptables_unavailable": "Non puoi giocare con iptables qui. O sei in un container o il tuo kernel non lo supporta",
- "ldap_init_failed_to_create_admin": "L'inizializzazione LDAP non è riuscita a creare un utente admin",
- "mail_alias_remove_failed": "Impossibile rimuovere l'alias mail '{mail:s}'",
- "mail_domain_unknown": "Indirizzo mail non valido per il dominio '{domain:s}'. Usa un dominio gestito da questo server.",
- "mail_forward_remove_failed": "Impossibile rimuovere la mail inoltrata '{mail:s}'",
+ "mail_alias_remove_failed": "Impossibile rimuovere l'alias mail '{mail}'",
+ "mail_domain_unknown": "Indirizzo mail non valido per il dominio '{domain}'. Usa un dominio gestito da questo server.",
+ "mail_forward_remove_failed": "Impossibile rimuovere la mail inoltrata '{mail}'",
"mailbox_used_space_dovecot_down": "La casella di posta elettronica Dovecot deve essere attivato se vuoi recuperare lo spazio usato dalla posta elettronica",
"main_domain_change_failed": "Impossibile cambiare il dominio principale",
"main_domain_changed": "Il dominio principale è stato cambiato",
- "no_internet_connection": "Il server non è collegato a Internet",
- "not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path:s}'",
- "package_unknown": "Pacchetto '{pkgname}' sconosciuto",
+ "not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path}'",
"packages_upgrade_failed": "Impossibile aggiornare tutti i pacchetti",
"pattern_backup_archive_name": "Deve essere un nome di file valido di massimo 30 caratteri di lunghezza, con caratteri alfanumerici e \"-_.\" come unica punteggiatura",
"pattern_domain": "Deve essere un nome di dominio valido (es. il-mio-dominio.org)",
@@ -116,100 +103,92 @@
"pattern_lastname": "Deve essere un cognome valido",
"pattern_password": "Deve contenere almeno 3 caratteri",
"pattern_port_or_range": "Deve essere un numero di porta valido (es. 0-65535) o una fascia di porte valida (es. 100:200)",
- "pattern_positive_number": "Deve essere un numero positivo",
"pattern_username": "Caratteri minuscoli alfanumerici o trattini bassi soli",
- "port_already_closed": "La porta {port:d} è già chiusa per le connessioni {ip_version:s}",
- "restore_already_installed_app": "Un'applicazione con l'ID '{app:s}' è già installata",
- "restore_app_failed": "Impossibile ripristinare l'applicazione '{app:s}'",
+ "port_already_closed": "La porta {port} è già chiusa per le connessioni {ip_version}",
+ "restore_already_installed_app": "Un'applicazione con l'ID '{app}' è già installata",
+ "app_restore_failed": "Impossibile ripristinare l'applicazione '{app}': {error}",
"restore_cleaning_failed": "Impossibile pulire la directory temporanea di ripristino",
"restore_complete": "Ripristino completo",
- "restore_confirm_yunohost_installed": "Sei sicuro di volere ripristinare un sistema già installato? {answers:s}",
+ "restore_confirm_yunohost_installed": "Sei sicuro di volere ripristinare un sistema già installato? {answers}",
"restore_failed": "Impossibile ripristinare il sistema",
"user_update_failed": "Impossibile aggiornare l'utente {user}: {error}",
- "restore_hook_unavailable": "Lo script di ripristino per '{part:s}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio",
+ "restore_hook_unavailable": "Lo script di ripristino per '{part}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio",
"restore_nothings_done": "Nulla è stato ripristinato",
- "restore_running_app_script": "Ripristino dell'app '{app:s}'…",
- "restore_running_hooks": "Esecuzione degli hook di ripristino…",
- "service_added": "Il servizio '{service:s}' è stato aggiunto",
- "service_already_started": "Il servizio '{service:s}' è già avviato",
- "service_already_stopped": "Il servizio '{service:s}' è già stato fermato",
- "service_disable_failed": "Impossibile disabilitare l'avvio al boot del servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
- "service_enable_failed": "Impossibile eseguire il servizio '{service:s}' al boot di sistema.\n\nRegistri di servizio recenti:{logs:s}",
- "service_enabled": "Il servizio '{service:s}' si avvierà automaticamente al boot di sistema.",
- "service_start_failed": "Impossibile eseguire il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
- "service_started": "Servizio '{service:s}' avviato",
- "service_stopped": "Servizio '{service:s}' fermato",
- "service_unknown": "Servizio '{service:s}' sconosciuto",
+ "restore_running_app_script": "Ripristino dell'app '{app}'...",
+ "restore_running_hooks": "Esecuzione degli hook di ripristino...",
+ "service_added": "Il servizio '{service}' è stato aggiunto",
+ "service_already_started": "Il servizio '{service}' è già avviato",
+ "service_already_stopped": "Il servizio '{service}' è già stato fermato",
+ "service_disable_failed": "Impossibile disabilitare l'avvio al boot del servizio '{service}'\n\nRegistri di servizio recenti:{logs}",
+ "service_enable_failed": "Impossibile eseguire il servizio '{service}' al boot di sistema.\n\nRegistri di servizio recenti:{logs}",
+ "service_enabled": "Il servizio '{service}' si avvierà automaticamente al boot di sistema.",
+ "service_start_failed": "Impossibile eseguire il servizio '{service}'\n\nRegistri di servizio recenti:{logs}",
+ "service_started": "Servizio '{service}' avviato",
+ "service_stopped": "Servizio '{service}' fermato",
+ "service_unknown": "Servizio '{service}' sconosciuto",
"ssowat_conf_generated": "La configurazione SSOwat rigenerata",
"ssowat_conf_updated": "Configurazione SSOwat aggiornata",
"system_upgraded": "Sistema aggiornato",
- "unbackup_app": "{app:s} non verrà salvata",
+ "unbackup_app": "{app} non verrà salvata",
"unexpected_error": "È successo qualcosa di inatteso: {error}",
"unlimit": "Nessuna quota",
"updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema...",
"upgrade_complete": "Aggiornamento completo",
"upnp_dev_not_found": "Nessuno supporto UPnP trovato",
- "upnp_disabled": "UPnP è stato disattivato",
- "upnp_enabled": "UPnP è stato attivato",
+ "upnp_disabled": "UPnP è disattivato",
+ "upnp_enabled": "UPnP è attivato",
"upnp_port_open_failed": "Impossibile aprire le porte attraverso UPnP",
"user_created": "Utente creato",
"user_creation_failed": "Impossibile creare l'utente {user}: {error}",
"user_deletion_failed": "Impossibile cancellare l'utente {user}: {error}",
- "user_home_creation_failed": "Impossibile creare la 'home' directory del utente",
- "user_unknown": "Utente sconosciuto: {user:s}",
+ "user_home_creation_failed": "Impossibile creare la home directory '{home}' del utente",
+ "user_unknown": "Utente sconosciuto: {user}",
"user_updated": "Info dell'utente cambiate",
"yunohost_already_installed": "YunoHost è già installato",
- "yunohost_ca_creation_failed": "Impossibile creare una certificate authority",
"yunohost_configured": "YunoHost ora è configurato",
"yunohost_installing": "Installazione di YunoHost...",
"yunohost_not_installed": "YunoHost non è correttamente installato. Esegui 'yunohost tools postinstall'",
"domain_cert_gen_failed": "Impossibile generare il certificato",
- "certmanager_attempt_to_replace_valid_cert": "Stai provando a sovrascrivere un certificato buono e valido per il dominio {domain:s}! (Usa --force per ignorare)",
- "certmanager_domain_unknown": "Dominio {domain:s} sconosciuto",
- "certmanager_domain_cert_not_selfsigned": "Il certificato per il dominio {domain:s} non è auto-firmato. Sei sicuro di volere sostituirlo? (Usa '--force')",
- "certmanager_certificate_fetching_or_enabling_failed": "Il tentativo di usare il nuovo certificato per {domain:s} non funziona...",
- "certmanager_attempt_to_renew_nonLE_cert": "Il certificato per il dominio {domain:s} non è emesso da Let's Encrypt. Impossibile rinnovarlo automaticamente!",
- "certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain:s} non è in scadenza! (Puoi usare --force per forzare se sai quel che stai facendo)",
- "certmanager_domain_http_not_working": "Il dominio {domain:s} non sembra accessibile attraverso HTTP. Verifica nella sezione 'Web' nella diagnosi per maggiori informazioni. (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)",
+ "certmanager_attempt_to_replace_valid_cert": "Stai provando a sovrascrivere un certificato buono e valido per il dominio {domain}! (Usa --force per ignorare)",
+ "certmanager_domain_cert_not_selfsigned": "Il certificato per il dominio {domain} non è auto-firmato. Sei sicuro di volere sostituirlo? (Usa '--force')",
+ "certmanager_certificate_fetching_or_enabling_failed": "Il tentativo di usare il nuovo certificato per {domain} non funziona...",
+ "certmanager_attempt_to_renew_nonLE_cert": "Il certificato per il dominio {domain} non è emesso da Let's Encrypt. Impossibile rinnovarlo automaticamente!",
+ "certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain} non è in scadenza! (Puoi usare --force per forzare se sai quel che stai facendo)",
+ "certmanager_domain_http_not_working": "Il dominio {domain} non sembra accessibile attraverso HTTP. Verifica nella sezione 'Web' nella diagnosi per maggiori informazioni. (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)",
"app_already_installed_cant_change_url": "Questa applicazione è già installata. L'URL non può essere cambiato solo da questa funzione. Controlla se `app changeurl` è disponibile.",
- "app_already_up_to_date": "{app:s} è già aggiornata",
- "app_change_url_failed_nginx_reload": "Non riesco a riavviare NGINX. Questo è il risultato di 'nginx -t':\n{nginx_errors:s}",
- "app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain:s}{path:s}'), nessuna operazione necessaria.",
- "app_change_url_no_script": "L'applicazione '{app_name:s}' non supporta ancora la modifica dell'URL. Forse dovresti aggiornarla.",
- "app_change_url_success": "L'URL dell'applicazione {app:s} è stato cambiato in {domain:s}{path:s}",
+ "app_already_up_to_date": "{app} è già aggiornata",
+ "app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain}{path}'), nessuna operazione necessaria.",
+ "app_change_url_no_script": "L'applicazione '{app_name}' non supporta ancora la modifica dell'URL. Forse dovresti aggiornarla.",
+ "app_change_url_success": "L'URL dell'applicazione {app} è stato cambiato in {domain}{path}",
"app_make_default_location_already_used": "Impostazione dell'applicazione '{app}' come predefinita del dominio non riuscita perché il dominio '{domain}' è in uso per dall'applicazione '{other_app}'",
- "app_location_unavailable": "Questo URL non è più disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}",
+ "app_location_unavailable": "Questo URL non è più disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps}",
"app_upgrade_app_name": "Aggiornamento di {app}...",
"app_upgrade_some_app_failed": "Alcune applicazioni non possono essere aggiornate",
"backup_abstract_method": "Questo metodo di backup deve essere ancora implementato",
- "backup_applying_method_borg": "Invio di tutti i file del backup nel deposito borg-backup...",
"backup_applying_method_copy": "Copiando tutti i files nel backup...",
- "backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method:s}'...",
+ "backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method}'...",
"backup_applying_method_tar": "Creando l'archivio TAR del backup...",
- "backup_archive_system_part_not_available": "La parte di sistema '{part:s}' non è disponibile in questo backup",
- "backup_archive_writing_error": "Impossibile aggiungere i file '{source:s}' (indicati nell'archivio '{dest:s}') al backup nell'archivio compresso '{archive:s}'",
- "backup_ask_for_copying_if_needed": "Vuoi effettuare il backup usando {size:s}MB temporaneamente? (È necessario usare questo sistema poiché alcuni file non possono essere preparati in un modo più efficiente)",
- "backup_borg_not_implemented": "Il metodo di backup Borg non è ancora stato implementato",
+ "backup_archive_system_part_not_available": "La parte di sistema '{part}' non è disponibile in questo backup",
+ "backup_archive_writing_error": "Impossibile aggiungere i file '{source}' (indicati nell'archivio '{dest}') al backup nell'archivio compresso '{archive}'",
+ "backup_ask_for_copying_if_needed": "Vuoi effettuare il backup usando {size}MB temporaneamente? (È necessario usare questo sistema poiché alcuni file non possono essere preparati in un modo più efficiente)",
"backup_cant_mount_uncompress_archive": "Impossibile montare in modalità sola lettura la cartella di archivio non compressa",
- "backup_copying_to_organize_the_archive": "Copiando {size:s}MB per organizzare l'archivio",
- "backup_couldnt_bind": "Impossibile legare {src:s} a {dest:s}.",
+ "backup_copying_to_organize_the_archive": "Copiando {size}MB per organizzare l'archivio",
+ "backup_couldnt_bind": "Impossibile legare {src} a {dest}.",
"backup_csv_addition_failed": "Impossibile aggiungere file del backup nel file CSV",
"backup_csv_creation_failed": "Impossibile creare il file CVS richiesto per le operazioni di ripristino",
"backup_custom_backup_error": "Il metodo di backup personalizzato è fallito allo step 'backup'",
"backup_custom_mount_error": "Il metodo di backup personalizzato è fallito allo step 'mount'",
- "backup_method_borg_finished": "Backup in borg terminato",
"backup_method_copy_finished": "Copia di backup terminata",
- "backup_method_custom_finished": "Metodo di backup personalizzato '{method:s}' terminato",
+ "backup_method_custom_finished": "Metodo di backup personalizzato '{method}' terminato",
"backup_method_tar_finished": "Archivio TAR di backup creato",
"backup_no_uncompress_archive_dir": "La cartella di archivio non compressa non esiste",
- "backup_php5_to_php7_migration_may_fail": "Conversione del tuo archivio per supportare php7 non riuscita, le tue app php potrebbero fallire in fase di ripristino (motivo: {error:s})",
- "backup_system_part_failed": "Impossibile creare il backup della parte di sistema '{part:s}'",
+ "backup_system_part_failed": "Impossibile creare il backup della parte di sistema '{part}'",
"backup_unable_to_organize_files": "Impossibile organizzare i file nell'archivio con il metodo veloce",
- "backup_with_no_backup_script_for_app": "L'app {app:s} non ha script di backup. Ignorata.",
- "backup_with_no_restore_script_for_app": "L'app {app:s} non ha script di ripristino, non sarai in grado di ripristinarla automaticamente dal backup di questa app.",
+ "backup_with_no_backup_script_for_app": "L'app {app} non ha script di backup. Ignorata.",
+ "backup_with_no_restore_script_for_app": "L'app {app} non ha script di ripristino, non sarai in grado di ripristinarla automaticamente dal backup di questa app.",
"certmanager_acme_not_configured_for_domain": "La challenge ACME non può validare il {domain} perché la relativa configurazione di nginx è mancante... Assicurati che la tua configurazione di nginx sia aggiornata con il comando `yunohost tools regen-conf nginx --dry-run --with-diff`.",
- "certmanager_cannot_read_cert": "Qualcosa è andato storto nel tentativo di aprire il certificato attuale per il dominio {domain:s} (file: {file:s}), motivo: {reason:s}",
- "certmanager_cert_install_success": "Certificato Let's Encrypt per il dominio {domain:s} installato",
+ "certmanager_cannot_read_cert": "Qualcosa è andato storto nel tentativo di aprire il certificato attuale per il dominio {domain} (file: {file}), motivo: {reason}",
+ "certmanager_cert_install_success": "Certificato Let's Encrypt per il dominio {domain} installato",
"aborting": "Annullamento.",
"admin_password_too_long": "Per favore scegli una password più corta di 127 caratteri",
"app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}",
@@ -222,8 +201,8 @@
"ask_new_path": "Nuovo percorso",
"backup_actually_backuping": "Creazione di un archivio di backup con i file raccolti...",
"backup_mount_archive_for_restore": "Preparazione dell'archivio per il ripristino…",
- "certmanager_cert_install_success_selfsigned": "Certificato autofirmato installato con successo per il dominio {domain:s}",
- "certmanager_cert_renew_success": "Certificato di Let's Encrypt rinnovato con successo per il dominio {domain:s}",
+ "certmanager_cert_install_success_selfsigned": "Certificato autofirmato installato con successo per il dominio {domain}",
+ "certmanager_cert_renew_success": "Certificato di Let's Encrypt rinnovato con successo per il dominio {domain}",
"certmanager_cert_signing_failed": "Impossibile firmare il nuovo certificato",
"good_practices_about_user_password": "Ora stai per impostare una nuova password utente. La password dovrebbe essere di almeno 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una sequenza di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).",
"password_listed": "Questa password è una tra le più utilizzate al mondo. Per favore scegline una più unica.",
@@ -231,54 +210,43 @@
"password_too_simple_2": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole",
"password_too_simple_3": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole e simboli",
"password_too_simple_4": "La password deve essere lunga almeno 12 caratteri e contenere numeri, maiuscole e minuscole",
- "users_available": "Utenti disponibili:",
- "yunohost_ca_creation_success": "Autorità di certificazione locale creata.",
"app_action_cannot_be_ran_because_required_services_down": "I seguenti servizi dovrebbero essere in funzione per completare questa azione: {services}. Prova a riavviarli per proseguire (e possibilmente cercare di capire come ma non funzionano più).",
- "backup_output_symlink_dir_broken": "La tua cartella d'archivio '{path:s}' è un link simbolico interrotto. Probabilmente hai dimenticato di montare o montare nuovamente il supporto al quale punta il link.",
- "certmanager_conflicting_nginx_file": "Impossibile preparare il dominio per il controllo ACME: il file di configurazione nginx {filepath:s} è in conflitto e dovrebbe essere prima rimosso",
- "certmanager_couldnt_fetch_intermediate_cert": "Tempo scaduto durante il tentativo di recupero di un certificato intermedio da Let's Encrypt. Installazione/rinnovo non riuscito - per favore riprova più tardi.",
- "certmanager_domain_dns_ip_differs_from_public_ip": "I record DNS per il dominio '{domain:s}' è diverso dall'IP di questo server. Controlla la sezione (basic) 'Record DNS' nella diagnosi per maggiori informazioni. Se hai modificato recentemente il tuo valore A, attendi che si propaghi (esistono online alcuni siti per il controllo della propagazione DNS). (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)",
- "certmanager_error_no_A_record": "Nessun valore DNS 'A' trovato per {domain:s}. Devi far puntare il tuo nome di dominio verso la tua macchina per essere in grado di installare un certificato Let's Encrypt! (Se sai cosa stai facendo, usa --no-checks per disabilitare quei controlli.)",
- "certmanager_hit_rate_limit": "Troppi certificati già rilasciati per questa esatta serie di domini {domain:s} recentemente. Per favore riprova più tardi. Guarda https://letsencrypt.org/docs/rate-limits/ per maggiori dettagli",
- "certmanager_http_check_timeout": "Tempo scaduto durante il tentativo di contatto del tuo server a se stesso attraverso HTTP utilizzando l'indirizzo IP pubblico (dominio {domain:s} con ip {ip:s}). Potresti avere un problema di hairpinning o il firewall/router davanti al tuo server non è correttamente configurato.",
- "certmanager_no_cert_file": "Impossibile leggere il file di certificato per il dominio {domain:s} (file: {file:s})",
- "certmanager_self_ca_conf_file_not_found": "File di configurazione non trovato per l'autorità di auto-firma (file: {file:s})",
- "certmanager_unable_to_parse_self_CA_name": "Impossibile analizzare il nome dell'autorità di auto-firma (file: {file:s})",
- "confirm_app_install_warning": "Attenzione: Questa applicazione potrebbe funzionare, ma non è ben integrata in YunoHost. Alcune funzionalità come il single sign-on e il backup/ripristino potrebbero non essere disponibili. Installare comunque? [{answers:s}] ",
- "confirm_app_install_danger": "ATTENZIONE! Questa applicazione è ancora sperimentale (se non esplicitamente dichiarata non funzionante)! Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio,digita '{answers:s}'",
- "confirm_app_install_thirdparty": "PERICOLO! Quest'applicazione non fa parte del catalogo Yunohost. Installando app di terze parti potresti compromettere l'integrita e la sicurezza del tuo sistema. Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio, digita '{answers:s}'",
+ "backup_output_symlink_dir_broken": "La tua cartella d'archivio '{path}' è un link simbolico interrotto. Probabilmente hai dimenticato di montare o montare nuovamente il supporto al quale punta il link.",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "I record DNS per il dominio '{domain}' è diverso dall'IP di questo server. Controlla la sezione (basic) 'Record DNS' nella diagnosi per maggiori informazioni. Se hai modificato recentemente il tuo valore A, attendi che si propaghi (esistono online alcuni siti per il controllo della propagazione DNS). (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)",
+ "certmanager_hit_rate_limit": "Troppi certificati già rilasciati per questa esatta serie di domini {domain} recentemente. Per favore riprova più tardi. Guarda https://letsencrypt.org/docs/rate-limits/ per maggiori dettagli",
+ "certmanager_no_cert_file": "Impossibile leggere il file di certificato per il dominio {domain} (file: {file})",
+ "certmanager_self_ca_conf_file_not_found": "File di configurazione non trovato per l'autorità di auto-firma (file: {file})",
+ "certmanager_unable_to_parse_self_CA_name": "Impossibile analizzare il nome dell'autorità di auto-firma (file: {file})",
+ "confirm_app_install_warning": "Attenzione: Questa applicazione potrebbe funzionare, ma non è ben integrata in YunoHost. Alcune funzionalità come il single sign-on e il backup/ripristino potrebbero non essere disponibili. Installare comunque? [{answers}] ",
+ "confirm_app_install_danger": "ATTENZIONE! Questa applicazione è ancora sperimentale (se non esplicitamente dichiarata non funzionante)! Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio,digita '{answers}'",
+ "confirm_app_install_thirdparty": "PERICOLO! Quest'applicazione non fa parte del catalogo YunoHost. Installando app di terze parti potresti compromettere l'integrita e la sicurezza del tuo sistema. Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio, digita '{answers}'",
"dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/APT (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.",
- "domain_cannot_remove_main": "Non puoi rimuovere '{domain:s}' essendo il dominio principale, prima devi impostare un nuovo dominio principale con il comando 'yunohost domain main-domain -n '; ecco la lista dei domini candidati: {other_domains:s}",
+ "domain_cannot_remove_main": "Non puoi rimuovere '{domain}' essendo il dominio principale, prima devi impostare un nuovo dominio principale con il comando 'yunohost domain main-domain -n '; ecco la lista dei domini candidati: {other_domains}",
"domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.",
- "dyndns_could_not_check_provide": "Impossibile controllare se {provider:s} possano fornire {domain:s}.",
- "dyndns_could_not_check_available": "Impossibile controllare se {domain:s} è disponibile su {provider:s}.",
- "dyndns_domain_not_provided": "Il fornitore DynDNS {provider:s} non può fornire il dominio {domain:s}.",
+ "dyndns_could_not_check_provide": "Impossibile controllare se {provider} possano fornire {domain}.",
+ "dyndns_could_not_check_available": "Impossibile controllare se {domain} è disponibile su {provider}.",
+ "dyndns_domain_not_provided": "Il fornitore DynDNS {provider} non può fornire il dominio {domain}.",
"experimental_feature": "Attenzione: Questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.",
- "file_does_not_exist": "Il file {path:s} non esiste.",
- "global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting:s}, ricevuta '{choice:s}', ma le scelte disponibili sono: {available_choices:s}",
- "global_settings_bad_type_for_setting": "Tipo errato per l'impostazione {setting:s}, ricevuto {received_type:s}, atteso {expected_type:s}",
- "global_settings_cant_open_settings": "Apertura del file delle impostazioni non riuscita, motivo: {reason:s}",
- "global_settings_cant_serialize_settings": "Serializzazione dei dati delle impostazioni non riuscita, motivo: {reason:s}",
- "global_settings_cant_write_settings": "Scrittura del file delle impostazioni non riuscita, motivo: {reason:s}",
- "global_settings_key_doesnt_exists": "La chiave '{settings_key:s}' non esiste nelle impostazioni globali, puoi vedere tutte le chiavi disponibili eseguendo 'yunohost settings list'",
- "global_settings_reset_success": "Le impostazioni precedenti sono state salvate in {path:s}",
- "global_settings_setting_example_bool": "Esempio di opzione booleana",
- "global_settings_setting_example_enum": "Esempio di opzione enum",
+ "file_does_not_exist": "Il file {path} non esiste.",
+ "global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting}, ricevuta '{choice}', ma le scelte disponibili sono: {available_choices}",
+ "global_settings_bad_type_for_setting": "Tipo errato per l'impostazione {setting}, ricevuto {received_type}, atteso {expected_type}",
+ "global_settings_cant_open_settings": "Apertura del file delle impostazioni non riuscita, motivo: {reason}",
+ "global_settings_cant_serialize_settings": "Serializzazione dei dati delle impostazioni non riuscita, motivo: {reason}",
+ "global_settings_cant_write_settings": "Scrittura del file delle impostazioni non riuscita, motivo: {reason}",
+ "global_settings_key_doesnt_exists": "La chiave '{settings_key}' non esiste nelle impostazioni globali, puoi vedere tutte le chiavi disponibili eseguendo 'yunohost settings list'",
+ "global_settings_reset_success": "Le impostazioni precedenti sono state salvate in {path}",
"already_up_to_date": "Niente da fare. Tutto è già aggiornato.",
- "global_settings_setting_example_int": "Esempio di opzione int",
- "global_settings_setting_example_string": "Esempio di opzione string",
"global_settings_setting_security_nginx_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)",
"global_settings_setting_security_password_admin_strength": "Complessità della password di amministratore",
"global_settings_setting_security_password_user_strength": "Complessità della password utente",
"global_settings_setting_security_ssh_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)",
- "global_settings_unknown_setting_from_settings_file": "Chiave sconosciuta nelle impostazioni: '{setting_key:s}', scartata e salvata in /etc/yunohost/settings-unknown.json",
+ "global_settings_unknown_setting_from_settings_file": "Chiave sconosciuta nelle impostazioni: '{setting_key}', scartata e salvata in /etc/yunohost/settings-unknown.json",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del hostkey DSA (deprecato) per la configurazione del demone SSH",
- "global_settings_unknown_type": "Situazione inaspettata, l'impostazione {setting:s} sembra essere di tipo {unknown_type:s} ma non è un tipo supportato dal sistema.",
- "good_practices_about_admin_password": "Stai per definire una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).",
+ "global_settings_unknown_type": "Situazione inaspettata, l'impostazione {setting} sembra essere di tipo {unknown_type} ma non è un tipo supportato dal sistema.",
+ "good_practices_about_admin_password": "Stai per impostare una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).",
"log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}",
- "log_category_404": "La categoria di registrazione '{category}' non esiste",
"log_link_to_log": "Registro completo di questa operazione: '{desc}'",
- "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}{name}'",
+ "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}'",
"global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)",
"log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui",
"log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'",
@@ -311,29 +279,6 @@
"log_tools_shutdown": "Spegni il tuo server",
"log_tools_reboot": "Riavvia il tuo server",
"mail_unavailable": "Questo indirizzo email è riservato e dovrebbe essere automaticamente assegnato al primo utente",
- "migrate_tsig_end": "Migrazione a hmac-sha512 terminata",
- "migrate_tsig_failed": "Migrazione del dominio dyndns {domain} verso hmac-sha512 fallita, torno indetro. Errore: {error_code} - {error}",
- "migrate_tsig_start": "Trovato un algoritmo di chiave non abbastanza sicuro per la firma TSIG del dominio '{domain}', inizio della migrazione verso la più sicura hmac-sha512",
- "migrate_tsig_wait": "Aspetta 3 minuti che il server dyndns prenda la nuova chiave in gestione…",
- "migrate_tsig_wait_2": "2 minuti…",
- "migrate_tsig_wait_3": "1 minuto…",
- "migrate_tsig_wait_4": "30 secondi…",
- "migrate_tsig_not_needed": "Non sembra tu stia utilizzando un dominio dyndns, quindi non è necessaria nessuna migrazione!",
- "migration_description_0001_change_cert_group_to_sslcert": "Cambia permessi del gruppo di certificati da 'metronome' a 'ssl-cert'",
- "migration_description_0002_migrate_to_tsig_sha256": "Migliora la sicurezza del TSIG dyndns utilizzando SHA512 invece di MD5",
- "migration_description_0003_migrate_to_stretch": "Aggiorna il sistema a Debian Stretch e YunoHost 3.0",
- "migration_description_0004_php5_to_php7_pools": "Riconfigura le PHP pools ad utilizzare PHP 7 invece di 5",
- "migration_description_0005_postgresql_9p4_to_9p6": "Migra i database da postgresql 9.4 a 9.6",
- "migration_description_0006_sync_admin_and_root_passwords": "Sincronizza password di amministratore e root",
- "migration_description_0010_migrate_to_apps_json": "Rimuovi gli elenchi di app deprecati ed usa invece il nuovo elenco unificato 'apps.json'",
- "migration_0003_start": "Migrazione a Stretch iniziata. I registri saranno disponibili in {logfile}.",
- "migration_0003_patching_sources_list": "Sistemando il file sources.lists…",
- "migration_0003_main_upgrade": "Iniziando l'aggiornamento principale…",
- "migration_0003_fail2ban_upgrade": "Iniziando l'aggiornamento di fail2ban…",
- "migration_0003_restoring_origin_nginx_conf": "Il tuo file /etc/nginx/nginx.conf è stato modificato in qualche modo. La migrazione lo riporterà al suo stato originale… Il file precedente sarà disponibile come {backup_dest}.",
- "migration_0003_yunohost_upgrade": "Iniziando l'aggiornamento dei pacchetti yunohost… La migrazione terminerà, ma l'aggiornamento attuale avverrà subito dopo. Dopo che l'operazione sarà completata, probabilmente dovrai riaccedere all'interfaccia di amministrazione.",
- "migration_0003_not_jessie": "La distribuzione attuale non è Jessie!",
- "migration_0003_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Per favore prima esegui un aggiornamento normale prima di migrare a stretch.",
"this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/APT (i gestori di pacchetti del sistema)... Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.",
"app_action_broke_system": "Questa azione sembra avere rotto questi servizi importanti: {services}",
"app_remove_after_failed_install": "Rimozione dell'applicazione a causa del fallimento dell'installazione...",
@@ -351,7 +296,7 @@
"backup_archive_cant_retrieve_info_json": "Impossibile caricare informazione per l'archivio '{archive}'... Impossibile scaricare info.json (oppure non è un json valido).",
"app_packaging_format_not_supported": "Quest'applicazione non può essere installata perché il formato non è supportato dalla vostra versione di YunoHost. Dovreste considerare di aggiornare il vostro sistema.",
"certmanager_domain_not_diagnosed_yet": "Non c'è ancora alcun risultato di diagnosi per il dominio {domain}. Riavvia una diagnosi per la categoria 'DNS records' e 'Web' nella sezione di diagnosi per verificare se il dominio è pronto per Let's Encrypt. (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)",
- "backup_permission": "Backup dei permessi per {app:s}",
+ "backup_permission": "Backup dei permessi per {app}",
"ask_user_domain": "Dominio da usare per l'indirizzo email e l'account XMPP dell'utente",
"app_manifest_install_ask_is_public": "Quest'applicazione dovrà essere visibile ai visitatori anonimi?",
"app_manifest_install_ask_admin": "Scegli un utente amministratore per quest'applicazione",
@@ -359,17 +304,16 @@
"app_manifest_install_ask_path": "Scegli il percorso dove installare quest'applicazione",
"app_manifest_install_ask_domain": "Scegli il dominio dove installare quest'app",
"app_argument_password_no_default": "Errore durante il parsing dell'argomento '{name}': l'argomento password non può avere un valore di default per ragioni di sicurezza",
- "additional_urls_already_added": "L'URL aggiuntivo '{url:s}' è già utilizzato come URL aggiuntivo per il permesso '{permission:s}'",
+ "additional_urls_already_added": "L'URL aggiuntivo '{url}' è già utilizzato come URL aggiuntivo per il permesso '{permission}'",
"diagnosis_basesystem_ynh_inconsistent_versions": "Stai eseguendo versioni incompatibili dei pacchetti YunoHost... probabilmente a causa di aggiornamenti falliti o parziali.",
"diagnosis_basesystem_ynh_main_version": "Il server sta eseguendo YunoHost {main_version} ({repo})",
"diagnosis_basesystem_ynh_single_version": "Versione {package}: {version} ({repo})",
"diagnosis_basesystem_kernel": "Il server sta eseguendo Linux kernel {kernel_version}",
"diagnosis_basesystem_host": "Il server sta eseguendo Debian {debian_version}",
- "diagnosis_basesystem_hardware_board": "Il modello della scheda server è {model}",
"diagnosis_basesystem_hardware": "L'architettura hardware del server è {virt} {arch}",
- "certmanager_warning_subdomain_dns_record": "Il sottodominio '{subdomain:s}' non si risolve nello stesso indirizzo IP di '{domain:s}'. Alcune funzioni non saranno disponibili finchè questa cosa non verrà sistemata e rigenerato il certificato.",
+ "certmanager_warning_subdomain_dns_record": "Il sottodominio '{subdomain}' non si risolve nello stesso indirizzo IP di '{domain}'. Alcune funzioni non saranno disponibili finchè questa cosa non verrà sistemata e rigenerato il certificato.",
"app_label_deprecated": "Questo comando è deprecato! Utilizza il nuovo comando 'yunohost user permission update' per gestire la label dell'app.",
- "additional_urls_already_removed": "L'URL aggiuntivo '{url:s}' è già stato rimosso come URL aggiuntivo per il permesso '{permission:s}'",
+ "additional_urls_already_removed": "L'URL aggiuntivo '{url}' è già stato rimosso come URL aggiuntivo per il permesso '{permission}'",
"diagnosis_services_bad_status_tip": "Puoi provare a riavviare il servizio, e se non funziona, controlla ai log del servizio in amministrazione (dalla linea di comando, puoi farlo con yunohost service restart {service} e yunohost service log {service} ).",
"diagnosis_services_bad_status": "Il servizio {service} è {status} :(",
"diagnosis_services_conf_broken": "Il servizio {service} è mal-configurato!",
@@ -381,7 +325,7 @@
"diagnosis_domain_expiration_not_found_details": "Le informazioni WHOIS per il dominio {domain} non sembrano contenere la data di scadenza, giusto?",
"diagnosis_domain_not_found_details": "Il dominio {domain} non esiste nel database WHOIS o è scaduto!",
"diagnosis_domain_expiration_not_found": "Non riesco a controllare la data di scadenza di alcuni domini",
- "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da Yunohost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost dyndns update --force .",
+ "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost dyndns update --force .",
"diagnosis_dns_point_to_doc": "Controlla la documentazione a https://yunohost.org/dns_config se hai bisogno di aiuto nel configurare i record DNS.",
"diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}",
"diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {value}",
@@ -410,14 +354,14 @@
"diagnosis_cant_run_because_of_dep": "Impossibile lanciare la diagnosi per {category} mentre ci sono problemi importanti collegati a {dep}.",
"diagnosis_cache_still_valid": "(La cache della diagnosi di {category} è ancora valida. Non la ricontrollo di nuovo per ora!)",
"diagnosis_failed_for_category": "Diagnosi fallita per la categoria '{category}:{error}",
- "diagnosis_display_tip": "Per vedere i problemi rilevati, puoi andare alla sezione Diagnosi del amministratore, o eseguire 'yunohost diagnosis show --issues' dalla riga di comando.",
- "diagnosis_package_installed_from_sury_details": "Alcuni pacchetti sono stati inavvertitamente installati da un repository di terze parti chiamato Sury. Il team di Yunohost ha migliorato la gestione di tali pacchetti, ma ci si aspetta che alcuni setup di app PHP7.3 abbiano delle incompatibilità anche se sono ancora in Stretch. Per sistemare questa situazione, dovresti provare a lanciare il seguente comando: {cmd_to_fix} ",
+ "diagnosis_display_tip": "Per vedere i problemi rilevati, puoi andare alla sezione Diagnosi del amministratore, o eseguire 'yunohost diagnosis show --issues --human-readable' dalla riga di comando.",
+ "diagnosis_package_installed_from_sury_details": "Alcuni pacchetti sono stati inavvertitamente installati da un repository di terze parti chiamato Sury. Il team di YunoHost ha migliorato la gestione di tali pacchetti, ma ci si aspetta che alcuni setup di app PHP7.3 abbiano delle incompatibilità anche se sono ancora in Stretch. Per sistemare questa situazione, dovresti provare a lanciare il seguente comando: {cmd_to_fix} ",
"diagnosis_package_installed_from_sury": "Alcuni pacchetti di sistema dovrebbero fare il downgrade",
"diagnosis_mail_ehlo_bad_answer": "Un servizio diverso da SMTP ha risposto sulla porta 25 su IPv{ipversion}",
"diagnosis_mail_ehlo_unreachable_details": "Impossibile aprire una connessione sulla porta 25 sul tuo server su IPv{ipversion}. Sembra irraggiungibile.
1. La causa più probabile di questo problema è la porta 25 non correttamente inoltrata al tuo server.
2. Dovresti esser sicuro che il servizio postfix sia attivo.
3. Su setup complessi: assicuratu che nessun firewall o reverse-proxy stia interferendo.",
"diagnosis_mail_ehlo_unreachable": "Il server SMTP non è raggiungibile dall'esterno su IPv{ipversion}. Non potrà ricevere email.",
"diagnosis_mail_ehlo_ok": "Il server SMTP è raggiungibile dall'esterno e quindi può ricevere email!",
- "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alcuni provider non ti permettono di aprire la porta 25 in uscita perché non gli importa della Net Neutrality.
- Alcuni mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare per un provider pro Net Neutrality",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alcuni provider non ti permettono di aprire la porta 25 in uscita perché non gli importa della Net Neutrality.
- Alcuni mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare per un provider pro Net Neutrality",
"diagnosis_mail_outgoing_port_25_blocked_details": "Come prima cosa dovresti sbloccare la porta 25 in uscita dall'interfaccia del tuo router internet o del tuo hosting provider. (Alcuni hosting provider potrebbero richiedere l'invio di un ticket di supporto per la richiesta).",
"diagnosis_mail_outgoing_port_25_blocked": "Il server SMTP non può inviare email ad altri server perché la porta 25 è bloccata in uscita su IPv{ipversion}.",
"diagnosis_mail_outgoing_port_25_ok": "Il server SMTP è abile all'invio delle email (porta 25 in uscita non bloccata).",
@@ -438,34 +382,34 @@
"diagnosis_mail_ehlo_could_not_diagnose": "Non è possibile verificare se il server mail postfix è raggiungibile dall'esterno su IPv{ipversion}.",
"diagnosis_mail_ehlo_wrong": "Un server mail SMTP diverso sta rispondendo su IPv{ipversion}. Probabilmente il tuo server non può ricevere email.",
"diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server.",
- "diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona...). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:
- Alcuni ISP mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona...). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:
- Alcuni ISP mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider",
"diagnosis_mail_ehlo_wrong_details": "L'EHLO ricevuto dalla diagnostica remota su IPv{ipversion} è differente dal dominio del tuo server.
EHLO ricevuto: {wrong_ehlo}
EHLO atteso: {right_ehlo}
La causa più comune di questo problema è la porta 25 non correttamente inoltrata al tuo server. Oppure assicurati che nessun firewall o reverse-proxy stia interferendo.",
"diagnosis_mail_blacklist_ok": "Gli IP e i domini utilizzati da questo server non sembrano essere nelle blacklist",
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS invero corrente: {rdns_domain}
Valore atteso: {ehlo_domain}
",
"diagnosis_mail_fcrdns_different_from_ehlo_domain": "Il DNS inverso non è correttamente configurato su IPv{ipversion}. Alcune email potrebbero non essere spedite o segnalate come SPAM.",
"diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene...). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo yunohost settings set smtp.allow_ipv6 -v off . NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti.",
- "yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- aggiungere il primo utente nella sezione 'Utenti' del webadmin (o eseguendo da terminale 'yunohost user create ');\n\t- eseguire una diagnosi alla ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know Yunohost' nella documentazione admin: https://yunohost.org/admindoc.",
+ "yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- aggiungere il primo utente nella sezione 'Utenti' del webadmin (o eseguendo da terminale 'yunohost user create ');\n\t- eseguire una diagnosi alla ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know YunoHost' nella documentazione admin: https://yunohost.org/admindoc.",
"user_already_exists": "L'utente '{user}' esiste già",
"update_apt_cache_warning": "Qualcosa è andato storto mentre eseguivo l'aggiornamento della cache APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}",
"update_apt_cache_failed": "Impossibile aggiornare la cache di APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}",
"unknown_main_domain_path": "Percorso o dominio sconosciuto per '{app}'. Devi specificare un dominio e un percorso per poter specificare un URL per il permesso.",
"tools_upgrade_special_packages_completed": "Aggiornamento pacchetti YunoHost completato.\nPremi [Invio] per tornare al terminale",
"tools_upgrade_special_packages_explanation": "L'aggiornamento speciale continuerà in background. Per favore non iniziare nessun'altra azione sul tuo server per i prossimi ~10 minuti (dipende dalla velocità hardware). Dopo questo, dovrai ri-loggarti nel webadmin. Il registro di aggiornamento sarà disponibile in Strumenti → Log/Registri (nel webadmin) o dalla linea di comando eseguendo 'yunohost log list'.",
- "tools_upgrade_special_packages": "Adesso aggiorno i pacchetti 'speciali' (correlati a yunohost)…",
+ "tools_upgrade_special_packages": "Adesso aggiorno i pacchetti 'speciali' (correlati a yunohost)...",
"tools_upgrade_regular_packages_failed": "Impossibile aggiornare i pacchetti: {packages_list}",
- "tools_upgrade_regular_packages": "Adesso aggiorno i pacchetti 'normali' (non correlati a yunohost)…",
- "tools_upgrade_cant_unhold_critical_packages": "Impossibile annullare il blocco dei pacchetti critici/importanti…",
- "tools_upgrade_cant_hold_critical_packages": "Impossibile bloccare i pacchetti critici/importanti…",
+ "tools_upgrade_regular_packages": "Adesso aggiorno i pacchetti 'normali' (non correlati a yunohost)...",
+ "tools_upgrade_cant_unhold_critical_packages": "Impossibile annullare il blocco dei pacchetti critici/importanti...",
+ "tools_upgrade_cant_hold_critical_packages": "Impossibile bloccare i pacchetti critici/importanti...",
"tools_upgrade_cant_both": "Impossibile aggiornare sia il sistema e le app nello stesso momento",
- "tools_upgrade_at_least_one": "Specifica '--apps', o '--system'",
+ "tools_upgrade_at_least_one": "Specifica 'apps', o 'system'",
"show_tile_cant_be_enabled_for_regex": "Non puoi abilitare 'show_tile' in questo momento, perché l'URL del permesso '{permission}' è una regex",
"show_tile_cant_be_enabled_for_url_not_defined": "Non puoi abilitare 'show_tile' in questo momento, devi prima definire un URL per il permesso '{permission}'",
- "service_reloaded_or_restarted": "Il servizio '{service:s}' è stato ricaricato o riavviato",
- "service_reload_or_restart_failed": "Impossibile ricaricare o riavviare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}",
- "service_restarted": "Servizio '{service:s}' riavviato",
- "service_restart_failed": "Impossibile riavviare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}",
- "service_reloaded": "Servizio '{service:s}' ricaricato",
- "service_reload_failed": "Impossibile ricaricare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}",
+ "service_reloaded_or_restarted": "Il servizio '{service}' è stato ricaricato o riavviato",
+ "service_reload_or_restart_failed": "Impossibile ricaricare o riavviare il servizio '{service}'\n\nUltimi registri del servizio: {logs}",
+ "service_restarted": "Servizio '{service}' riavviato",
+ "service_restart_failed": "Impossibile riavviare il servizio '{service}'\n\nUltimi registri del servizio: {logs}",
+ "service_reloaded": "Servizio '{service}' ricaricato",
+ "service_reload_failed": "Impossibile ricaricare il servizio '{service}'\n\nUltimi registri del servizio: {logs}",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' è obsoleto! Per favore usa 'yunohost tools regen-conf' al suo posto.",
"service_description_yunohost-firewall": "Gestisce l'apertura e la chiusura delle porte ai servizi",
"service_description_yunohost-api": "Gestisce l'interazione tra l'interfaccia web YunoHost ed il sistema",
@@ -481,25 +425,24 @@
"service_description_fail2ban": "Ti protegge dal brute-force e altri tipi di attacchi da Internet",
"service_description_dovecot": "Consente ai client mail di accedere/recuperare le email (via IMAP e POP3)",
"service_description_dnsmasq": "Gestisce la risoluzione dei domini (DNS)",
- "service_description_avahi-daemon": "Consente di raggiungere il tuo server eseguendo 'yunohost.local' sulla tua LAN",
- "server_reboot_confirm": "Il server si riavvierà immediatamente, sei sicuro? [{answers:s}]",
+ "server_reboot_confirm": "Il server si riavvierà immediatamente, sei sicuro? [{answers}]",
"server_reboot": "Il server si riavvierà",
- "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers:s}]",
+ "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers}]",
"server_shutdown": "Il server si spegnerà",
"root_password_replaced_by_admin_password": "La tua password di root è stata sostituita dalla tua password d'amministratore.",
"root_password_desynchronized": "La password d'amministratore è stata cambiata, ma YunoHost non ha potuto propagarla alla password di root!",
- "restore_system_part_failed": "Impossibile ripristinare la sezione di sistema '{part:s}'",
+ "restore_system_part_failed": "Impossibile ripristinare la sezione di sistema '{part}'",
"restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea",
- "restore_not_enough_disk_space": "Spazio libero insufficiente (spazio: {free_space:d}B, necessario: {needed_space:d}B, margine di sicurezza: {margin:d}B)",
- "restore_may_be_not_enough_disk_space": "Il tuo sistema non sembra avere abbastanza spazio (libero: {free_space:d}B, necessario: {needed_space:d}B, margine di sicurezza: {margin:d}B)",
- "restore_extracting": "Sto estraendo i file necessari dall'archivio…",
+ "restore_not_enough_disk_space": "Spazio libero insufficiente (spazio: {free_space}B, necessario: {needed_space}B, margine di sicurezza: {margin}B)",
+ "restore_may_be_not_enough_disk_space": "Il tuo sistema non sembra avere abbastanza spazio (libero: {free_space}B, necessario: {needed_space}B, margine di sicurezza: {margin}B)",
+ "restore_extracting": "Sto estraendo i file necessari dall'archivio...",
"restore_already_installed_apps": "Le seguenti app non possono essere ripristinate perché sono già installate: {apps}",
"regex_with_only_domain": "Non puoi usare una regex per il dominio, solo per i percorsi",
"regex_incompatible_with_tile": "/!\\ Packagers! Il permesso '{permission}' ha show_tile impostato su 'true' e perciò non è possibile definire un URL regex per l'URL principale",
"regenconf_need_to_explicitly_specify_ssh": "La configurazione ssh è stata modificata manualmente, ma devi specificare la categoria 'ssh' con --force per applicare le modifiche.",
"regenconf_pending_applying": "Applico le configurazioni in attesa per la categoria '{category}'...",
"regenconf_failed": "Impossibile rigenerare la configurazione per le categorie: {categories}",
- "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'…",
+ "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'...",
"regenconf_would_be_updated": "La configurazione sarebbe stata aggiornata per la categoria '{category}'",
"regenconf_updated": "Configurazione aggiornata per '{category}'",
"regenconf_up_to_date": "Il file di configurazione è già aggiornato per la categoria '{category}'",
@@ -514,14 +457,14 @@
"regenconf_file_backed_up": "File di configurazione '{conf}' salvato in '{backup}'",
"permission_require_account": "Il permesso {permission} ha senso solo per gli utenti con un account, quindi non può essere attivato per i visitatori.",
"permission_protected": "Il permesso {permission} è protetto. Non puoi aggiungere o rimuovere il gruppo visitatori dal permesso.",
- "permission_updated": "Permesso '{permission:s}' aggiornato",
+ "permission_updated": "Permesso '{permission}' aggiornato",
"permission_update_failed": "Impossibile aggiornare il permesso '{permission}': {error}",
- "permission_not_found": "Permesso '{permission:s}' non trovato",
+ "permission_not_found": "Permesso '{permission}' non trovato",
"permission_deletion_failed": "Impossibile cancellare il permesso '{permission}': {error}",
- "permission_deleted": "Permesso '{permission:s}' cancellato",
+ "permission_deleted": "Permesso '{permission}' cancellato",
"permission_currently_allowed_for_all_users": "Il permesso è attualmente garantito a tutti gli utenti oltre gli altri gruppi. Probabilmente vuoi o rimuovere il permesso 'all_user' o rimuovere gli altri gruppi per cui è garantito attualmente.",
"permission_creation_failed": "Impossibile creare i permesso '{permission}': {error}",
- "permission_created": "Permesso '{permission:s}' creato",
+ "permission_created": "Permesso '{permission}' creato",
"permission_cannot_remove_main": "Non è possibile rimuovere un permesso principale",
"permission_already_up_to_date": "Il permesso non è stato aggiornato perché la richiesta di aggiunta/rimozione è già coerente con lo stato attuale.",
"permission_already_exist": "Permesso '{permission}' esiste già",
@@ -550,10 +493,6 @@
"migrations_cant_reach_migration_file": "Impossibile accedere ai file di migrazione nel path '%s'",
"migrations_already_ran": "Migrazioni già effettuate: {ids}",
"migration_0019_slapd_config_will_be_overwritten": "Sembra che tu abbia modificato manualmente la configurazione slapd. Per questa importante migrazione, YunoHost deve forzare l'aggiornamento della configurazione slapd. I file originali verranno back-uppati in {conf_backup_folder}.",
- "migration_0019_rollback_success": "Sistema ripristinato.",
- "migration_0019_migration_failed_trying_to_rollback": "Impossibile migrare... sto cercando di ripristinare il sistema.",
- "migration_0019_can_not_backup_before_migration": "Il backup del sistema non è stato completato prima della migrazione. Errore: {error:s}",
- "migration_0019_backup_before_migration": "Creando un backup del database LDAP e delle impostazioni delle app prima dell'effettiva migrazione.",
"migration_0019_add_new_attributes_in_ldap": "Aggiungi nuovi attributi ai permessi nel database LDAP",
"migration_0018_failed_to_reset_legacy_rules": "Impossibile resettare le regole iptables legacy: {error}",
"migration_0018_failed_to_migrate_iptables_rules": "Migrazione fallita delle iptables legacy a nftables: {error}",
@@ -574,39 +513,31 @@
"migration_0015_main_upgrade": "Inizio l'aggiornamento principale...",
"migration_0015_patching_sources_list": "Applico le patch a sources.lists...",
"migration_0015_start": "Inizio migrazione a Buster",
- "migration_0011_failed_to_remove_stale_object": "Impossibile rimuovere l'oggetto {dn}: {error}",
- "migration_0011_update_LDAP_schema": "Aggiornado lo schema LDAP...",
- "migration_0011_update_LDAP_database": "Aggiornando il database LDAP...",
- "migration_0011_migrate_permission": "Migrando permessi dalle impostazioni delle app a LDAP...",
- "migration_0011_LDAP_update_failed": "Impossibile aggiornare LDAP. Errore: {error:s}",
- "migration_0011_create_group": "Sto creando un gruppo per ogni utente...",
"migration_description_0019_extend_permissions_features": "Estendi il sistema di gestione dei permessi app",
"migration_description_0018_xtable_to_nftable": "Migra le vecchie regole di traffico network sul nuovo sistema nftable",
"migration_description_0017_postgresql_9p6_to_11": "Migra i database da PostgreSQL 9.6 a 11",
"migration_description_0016_php70_to_php73_pools": "MIgra i file di configurazione 'pool' di php7.0-fpm su php7.3",
"migration_description_0015_migrate_to_buster": "Aggiorna il sistema a Debian Buster e YunoHost 4.X",
"migrating_legacy_permission_settings": "Impostando le impostazioni legacy dei permessi..",
- "mailbox_disabled": "E-mail disabilitate per l'utente {user:s}",
+ "mailbox_disabled": "E-mail disabilitate per l'utente {user}",
"log_user_permission_reset": "Resetta il permesso '{}'",
"log_user_permission_update": "Aggiorna gli accessi del permesso '{}'",
"log_user_group_update": "Aggiorna il gruppo '{}'",
"log_user_group_delete": "Cancella il gruppo '{}'",
- "log_user_group_create": "Crea il gruppo '[}'",
+ "log_user_group_create": "Crea il gruppo '{}'",
"log_permission_url": "Aggiorna l'URL collegato al permesso '{}'",
"log_permission_delete": "Cancella permesso '{}'",
"log_permission_create": "Crea permesso '{}'",
- "log_app_config_apply": "Applica la configurazione all'app '{}'",
- "log_app_config_show_panel": "Mostra il pannello di configurazione dell'app '{}'",
"log_app_action_run": "Esegui l'azione dell'app '{}'",
"log_operation_unit_unclosed_properly": "Operazion unit non è stata chiusa correttamente",
- "invalid_regex": "Regex invalida:'{regex:s}'",
+ "invalid_regex": "Regex invalida:'{regex}'",
"hook_list_by_invalid": "Questa proprietà non può essere usata per listare gli hooks",
- "hook_json_return_error": "Impossibile leggere la risposta del hook {path:s}. Errore: {msg:s}. Contenuto raw: {raw_content}",
+ "hook_json_return_error": "Impossibile leggere la risposta del hook {path}. Errore: {msg}. Contenuto raw: {raw_content}",
"group_user_not_in_group": "L'utente {user} non è nel gruppo {group}",
"group_user_already_in_group": "L'utente {user} è già nel gruppo {group}",
"group_update_failed": "Impossibile aggiornare il gruppo '{group}': {error}",
"group_updated": "Gruppo '{group}' aggiornato",
- "group_unknown": "Gruppo '{group:s}' sconosciuto",
+ "group_unknown": "Gruppo '{group}' sconosciuto",
"group_deletion_failed": "Impossibile cancellare il gruppo '{group}': {error}",
"group_deleted": "Gruppo '{group}' cancellato",
"group_cannot_be_deleted": "Il gruppo {group} non può essere eliminato manualmente.",
@@ -628,7 +559,7 @@
"dyndns_provider_unreachable": "Incapace di raggiungere il provider DynDNS {provider}: o il tuo YunoHost non è connesso ad internet o il server dynette è down.",
"dpkg_lock_not_available": "Impossibile eseguire il comando in questo momento perché un altro programma sta bloccando dpkg (il package manager di sistema)",
"domain_name_unknown": "Dominio '{domain}' sconosciuto",
- "domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain:s}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add ', impostarlo come dominio principale con 'yunohost domain main-domain n ', e solo allora potrai rimuovere il dominio '{domain:s}' eseguendo 'yunohost domain remove {domain:s}'.'",
+ "domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add ', impostarlo come dominio principale con 'yunohost domain main-domain n ', e solo allora potrai rimuovere il dominio '{domain}' eseguendo 'yunohost domain remove {domain}'.'",
"domain_cannot_add_xmpp_upload": "Non puoi aggiungere domini che iniziano per 'xmpp-upload.'. Questo tipo di nome è riservato per la funzionalità di upload XMPP integrata in YunoHost.",
"diagnosis_processes_killed_by_oom_reaper": "Alcuni processi sono stati terminati dal sistema che era a corto di memoria. Questo è un sintomo di insufficienza di memoria nel sistema o di un processo che richiede troppa memoria. Lista dei processi terminati:\n{kills_summary}",
"diagnosis_never_ran_yet": "Sembra che questo server sia stato impostato recentemente e non è presente nessun report di diagnostica. Dovresti partire eseguendo una diagnostica completa, da webadmin o da terminale con il comando 'yunohost diagnosis run'.",
@@ -674,5 +605,29 @@
"diagnosis_mail_blacklist_reason": "Il motivo della blacklist è: {reason}",
"diagnosis_mail_blacklist_listed_by": "Il tuo IP o dominio {item}
è nella blacklist {blacklist_name}",
"diagnosis_backports_in_sources_list": "Sembra che apt (il package manager) sia configurato per utilizzare le backport del repository. A meno che tu non sappia quello che stai facendo, scoraggiamo fortemente di installare pacchetti tramite esse, perché ci sono alte probabilità di creare conflitti con il tuo sistema.",
- "diagnosis_basesystem_hardware_model": "Modello server: {model}"
-}
+ "diagnosis_basesystem_hardware_model": "Modello server: {model}",
+ "postinstall_low_rootfsspace": "La radice del filesystem ha uno spazio totale inferiore ai 10 GB, ed è piuttosto preoccupante! Consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem. Se vuoi installare YunoHost ignorando questo avviso, esegui nuovamente il postinstall con l'argomento --force-diskspace",
+ "domain_remove_confirm_apps_removal": "Rimuovere questo dominio rimuoverà anche le seguenti applicazioni:\n{apps}\n\nSei sicuro di voler continuare? [{answers}]",
+ "diagnosis_rootfstotalspace_critical": "La radice del filesystem ha un totale di solo {space}, ed è piuttosto preoccupante! Probabilmente consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem.",
+ "diagnosis_rootfstotalspace_warning": "La radice del filesystem ha un totale di solo {space}. Potrebbe non essere un problema, ma stai attento perché potresti consumare tutta la memoria velocemente... Raccomandiamo di avere almeno 16 GB per la radice del filesystem.",
+ "restore_backup_too_old": "Questo archivio backup non può essere ripristinato perché è stato generato da una versione troppo vecchia di YunoHost.",
+ "permission_cant_add_to_all_users": "Il permesso {permission} non può essere aggiunto a tutto gli utenti.",
+ "migration_update_LDAP_schema": "Aggiorno lo schema LDAP...",
+ "migration_ldap_rollback_success": "Sistema ripristinato allo stato precedente.",
+ "migration_ldap_migration_failed_trying_to_rollback": "Impossibile migrare... provo a ripristinare il sistema.",
+ "migration_ldap_can_not_backup_before_migration": "Il backup del sistema non è stato completato prima che la migrazione fallisse. Errore: {error}",
+ "migration_ldap_backup_before_migration": "Sto generando il backup del database LDAP e delle impostazioni delle app prima di effettuare la migrazione.",
+ "migration_description_0020_ssh_sftp_permissions": "Aggiungi il supporto ai permessi SSH e SFTP",
+ "log_backup_create": "Crea un archivio backup",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "Abilita il pannello sovrapposto SSOwat",
+ "global_settings_setting_security_ssh_port": "Porta SSH",
+ "diagnosis_sshd_config_inconsistent_details": "Esegui yunohost settings set security.ssh.port -v PORTA_SSH per definire la porta SSH, e controlla con yunohost tools regen-conf ssh --dry-run --with-diff , poi yunohost tools regen-conf ssh --force per resettare la tua configurazione con le raccomandazioni YunoHost.",
+ "diagnosis_sshd_config_inconsistent": "Sembra che la porta SSH sia stata modificata manualmente in /etc/ssh/sshd_config: A partire da YunoHost 4.2, una nuova configurazione globale 'security.ssh.port' è disponibile per evitare di modificare manualmente la configurazione.",
+ "diagnosis_sshd_config_insecure": "Sembra che la configurazione SSH sia stata modificata manualmente, ed non è sicuro dato che non contiene le direttive 'AllowGroups' o 'Allowusers' che limitano l'accesso agli utenti autorizzati.",
+ "backup_create_size_estimation": "L'archivio conterrà circa {size} di dati.",
+ "app_restore_script_failed": "C'è stato un errore all'interno dello script di recupero",
+ "global_settings_setting_security_webadmin_allowlist": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.",
+ "global_settings_setting_security_webadmin_allowlist_enabled": "Permetti solo ad alcuni IP di accedere al webadmin.",
+ "disk_space_not_sufficient_update": "Non c'è abbastanza spazio libero per aggiornare questa applicazione",
+ "disk_space_not_sufficient_install": "Non c'è abbastanza spazio libero per installare questa applicazione"
+}
\ No newline at end of file
diff --git a/locales/mk.json b/locales/mk.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/locales/mk.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/locales/nb_NO.json b/locales/nb_NO.json
index 66cefad04..dc217d74e 100644
--- a/locales/nb_NO.json
+++ b/locales/nb_NO.json
@@ -4,78 +4,67 @@
"admin_password_change_failed": "Kan ikke endre passord",
"admin_password_changed": "Administrasjonspassord endret",
"admin_password_too_long": "Velg et passord kortere enn 127 tegn",
- "app_already_installed": "{app:s} er allerede installert",
- "app_already_up_to_date": "{app:s} er allerede oppdatert",
- "app_argument_invalid": "Velg en gydlig verdi for argumentet '{name:s}': {error:s}",
- "app_argument_required": "Argumentet '{name:s}' er påkrevd",
+ "app_already_installed": "{app} er allerede installert",
+ "app_already_up_to_date": "{app} er allerede oppdatert",
+ "app_argument_invalid": "Velg en gydlig verdi for argumentet '{name}': {error}",
+ "app_argument_required": "Argumentet '{name}' er påkrevd",
"app_id_invalid": "Ugyldig program-ID",
- "dyndns_cron_remove_failed": "Kunne ikke fjerne cron-jobb for DynDNS: {error}",
"dyndns_key_not_found": "Fant ikke DNS-nøkkel for domenet",
- "app_not_correctly_installed": "{app:s} ser ikke ut til å ha blitt installert på riktig måte",
+ "app_not_correctly_installed": "{app} ser ikke ut til å ha blitt installert på riktig måte",
"dyndns_provider_unreachable": "Kunne ikke nå DynDNS-tilbyder {provider}: Enten har du ikke satt opp din YunoHost rett, dynette-tjeneren er nede, eller du mangler nett.",
- "app_not_properly_removed": "{app:s} har ikke blitt fjernet på riktig måte",
- "app_removed": "{app:s} fjernet",
- "app_requirements_checking": "Sjekker påkrevde pakker for {app:s}…",
+ "app_not_properly_removed": "{app} har ikke blitt fjernet på riktig måte",
+ "app_removed": "{app} fjernet",
+ "app_requirements_checking": "Sjekker påkrevde pakker for {app}…",
"app_start_install": "Installerer programmet '{app}'…",
- "action_invalid": "Ugyldig handling '{action:s}'",
+ "action_invalid": "Ugyldig handling '{action}'",
"app_start_restore": "Gjenoppretter programmet '{app}'…",
"backup_created": "Sikkerhetskopi opprettet",
"backup_archive_name_exists": "En sikkerhetskopi med dette navnet finnes allerede.",
- "backup_archive_name_unknown": "Ukjent lokalt sikkerhetskopiarkiv ved navn '{name:s}'",
+ "backup_archive_name_unknown": "Ukjent lokalt sikkerhetskopiarkiv ved navn '{name}'",
"already_up_to_date": "Ingenting å gjøre. Alt er oppdatert.",
"backup_method_copy_finished": "Sikkerhetskopi fullført",
"backup_method_tar_finished": "TAR-sikkerhetskopiarkiv opprettet",
"app_action_cannot_be_ran_because_required_services_down": "Dette programmet krever noen tjenester som ikke kjører. Før du fortsetter, du bør prøve å starte følgende tjenester på ny (og antagelig undersøke hvorfor de er nede): {services}",
"app_already_installed_cant_change_url": "Dette programmet er allerede installert. Nettadressen kan ikke endres kun med denne funksjonen. Ta en titt på `app changeurl` hvis den er tilgjengelig.",
"domain_exists": "Domenet finnes allerede",
- "app_change_url_failed_nginx_reload": "Kunne ikke gjeninnlaste NGINX. Her har du utdataen for 'nginx -t'\n{nginx_errors:s}",
"domains_available": "Tilgjengelige domener:",
"done": "Ferdig",
"downloading": "Laster ned…",
- "dyndns_could_not_check_provide": "Kunne ikke sjekke om {provider:s} kan tilby {domain:s}.",
- "dyndns_could_not_check_available": "Kunne ikke sjekke om {domain:s} er tilgjengelig på {provider:s}.",
- "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain:s}'",
- "migrate_tsig_wait_2": "2 min…",
+ "dyndns_could_not_check_provide": "Kunne ikke sjekke om {provider} kan tilby {domain}.",
+ "dyndns_could_not_check_available": "Kunne ikke sjekke om {domain} er tilgjengelig på {provider}.",
+ "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain}'",
"log_remove_on_failed_restore": "Fjern '{}' etter mislykket gjenoppretting fra sikkerhetskopiarkiv",
"log_letsencrypt_cert_install": "Installer et Let's Encrypt-sertifikat på '{}'-domenet",
"log_letsencrypt_cert_renew": "Forny '{}'-Let's Encrypt-sertifikat",
"log_user_update": "Oppdater brukerinfo for '{}'",
- "mail_alias_remove_failed": "Kunne ikke fjerne e-postaliaset '{mail:s}'",
+ "mail_alias_remove_failed": "Kunne ikke fjerne e-postaliaset '{mail}'",
"app_action_broke_system": "Denne handlingen ser ut til å ha knekt disse viktige tjenestene: {services}",
- "app_argument_choice_invalid": "Bruk én av disse valgene '{choices:s}' for argumentet '{name:s}'",
+ "app_argument_choice_invalid": "Bruk én av disse valgene '{choices}' for argumentet '{name}'",
"app_extraction_failed": "Kunne ikke pakke ut installasjonsfilene",
"app_install_files_invalid": "Disse filene kan ikke installeres",
"backup_abstract_method": "Denne sikkerhetskopimetoden er ikke implementert enda",
"backup_actually_backuping": "Oppretter sikkerhetskopiarkiv fra innsamlede filer…",
- "backup_app_failed": "Kunne ikke sikkerhetskopiere programmet '{app:s}'",
+ "backup_app_failed": "Kunne ikke sikkerhetskopiere programmet '{app}'",
"backup_applying_method_tar": "Lager TAR-sikkerhetskopiarkiv…",
- "backup_archive_app_not_found": "Fant ikke programmet '{app:s}' i sikkerhetskopiarkivet",
+ "backup_archive_app_not_found": "Fant ikke programmet '{app}' i sikkerhetskopiarkivet",
"backup_archive_open_failed": "Kunne ikke åpne sikkerhetskopiarkivet",
"app_start_remove": "Fjerner programmet '{app}'…",
"app_start_backup": "Samler inn filer for sikkerhetskopiering for {app}…",
"backup_applying_method_copy": "Kopier alle filer til sikkerhetskopi…",
- "backup_borg_not_implemented": "Borg-sikkerhetskopimetoden er ikke implementert enda",
"backup_creation_failed": "Kunne ikke opprette sikkerhetskopiarkiv",
- "backup_couldnt_bind": "Kunne ikke binde {src:s} til {dest:s}.",
+ "backup_couldnt_bind": "Kunne ikke binde {src} til {dest}.",
"backup_csv_addition_failed": "Kunne ikke legge til filer for sikkerhetskopi inn i CSV-filen",
"backup_deleted": "Sikkerhetskopi slettet",
"backup_no_uncompress_archive_dir": "Det finnes ingen slik utpakket arkivmappe",
- "backup_delete_error": "Kunne ikke slette '{path:s}'",
- "certmanager_domain_unknown": "Ukjent domene '{domain:s}'",
+ "backup_delete_error": "Kunne ikke slette '{path}'",
"certmanager_cert_signing_failed": "Kunne ikke signere det nye sertifikatet",
- "executing_command": "Kjører kommendoen '{command:s}'…",
- "executing_script": "Kjører skriptet '{script:s}'…",
"extracting": "Pakker ut…",
"log_domain_add": "Legg til '{}'-domenet i systemoppsett",
"log_domain_remove": "Fjern '{}'-domenet fra systemoppsett",
"log_dyndns_subscribe": "Abonner på YunoHost-underdomenet '{}'",
"log_dyndns_update": "Oppdater IP-adressen tilknyttet ditt YunoHost-underdomene '{}'",
- "migrate_tsig_wait_3": "1 min…",
- "migrate_tsig_wait_4": "30 sekunder…",
- "backup_invalid_archive": "Dette er ikke et sikkerhetskopiarkiv",
"backup_nothings_done": "Ingenting å lagre",
- "backup_method_borg_finished": "Sikkerhetskopi inn i Borg fullført",
- "field_invalid": "Ugyldig felt '{:s}'",
+ "field_invalid": "Ugyldig felt '{}'",
"firewall_reloaded": "Brannmur gjeninnlastet",
"log_app_change_url": "Endre nettadresse for '{}'-programmet",
"log_app_install": "Installer '{}'-programmet",
@@ -87,22 +76,19 @@
"log_tools_reboot": "Utfør omstart av tjeneren din",
"apps_already_up_to_date": "Alle programmer allerede oppdatert",
"backup_mount_archive_for_restore": "Forbereder arkiv for gjenopprettelse…",
- "backup_copying_to_organize_the_archive": "Kopierer {size:s} MB for å organisere arkivet",
+ "backup_copying_to_organize_the_archive": "Kopierer {size} MB for å organisere arkivet",
"domain_cannot_remove_main": "Kan ikke fjerne hoveddomene. Sett et først",
"domain_cert_gen_failed": "Kunne ikke opprette sertifikat",
"domain_created": "Domene opprettet",
"domain_creation_failed": "Kunne ikke opprette domene",
"domain_dyndns_root_unknown": "Ukjent DynDNS-rotdomene",
- "domain_unknown": "Ukjent domene",
- "dyndns_cron_installed": "Opprettet cron-jobb for DynDNS",
- "dyndns_cron_removed": "Fjernet cron-jobb for DynDNS",
"dyndns_ip_update_failed": "Kunne ikke oppdatere IP-adresse til DynDNS",
"dyndns_ip_updated": "Oppdaterte din IP på DynDNS",
"dyndns_key_generating": "Oppretter DNS-nøkkel… Dette kan ta en stund.",
"dyndns_no_domain_registered": "Inget domene registrert med DynDNS",
"dyndns_registered": "DynDNS-domene registrert",
"global_settings_setting_security_password_admin_strength": "Admin-passordets styrke",
- "dyndns_registration_failed": "Kunne ikke registrere DynDNS-domene: {error:s}",
+ "dyndns_registration_failed": "Kunne ikke registrere DynDNS-domene: {error}",
"global_settings_setting_security_password_user_strength": "Brukerpassordets styrke",
"log_backup_restore_app": "Gjenopprett '{}' fra sikkerhetskopiarkiv",
"log_remove_on_failed_install": "Fjern '{}' etter mislykket installasjon",
@@ -110,15 +96,11 @@
"log_user_delete": "Slett '{}' bruker",
"log_user_group_delete": "Slett '{}' gruppe",
"log_user_group_update": "Oppdater '{}' gruppe",
- "ldap_init_failed_to_create_admin": "LDAP-igangsettelse kunne ikke opprette admin-bruker",
- "ldap_initialized": "LDAP-igangsatt",
- "migration_description_0003_migrate_to_stretch": "Oppgrader systemet til Debian Stretch og YunoHost 3.0",
"app_unknown": "Ukjent program",
"app_upgrade_app_name": "Oppgraderer {app}…",
- "app_upgrade_failed": "Kunne ikke oppgradere {app:s}",
+ "app_upgrade_failed": "Kunne ikke oppgradere {app}",
"app_upgrade_some_app_failed": "Noen programmer kunne ikke oppgraderes",
- "app_upgraded": "{app:s} oppgradert",
- "ask_email": "E-postadresse",
+ "app_upgraded": "{app} oppgradert",
"ask_firstname": "Fornavn",
"ask_lastname": "Etternavn",
"ask_main_domain": "Hoveddomene",
@@ -130,10 +112,9 @@
"domain_deleted": "Domene slettet",
"domain_deletion_failed": "Kunne ikke slette domene",
"domain_dyndns_already_subscribed": "Du har allerede abonnement på et DynDNS-domene",
- "log_category_404": "Loggkategorien '{category}' finnes ikke",
"log_link_to_log": "Full logg for denne operasjonen: '{desc}'",
- "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}{name}'",
+ "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}'",
"log_user_create": "Legg til '{}' bruker",
- "app_change_url_success": "{app:s} nettadressen er nå {domain:s}{path:s}",
+ "app_change_url_success": "{app} nettadressen er nå {domain}{path}",
"app_install_failed": "Kunne ikke installere {app}: {error}"
}
\ No newline at end of file
diff --git a/locales/ne.json b/locales/ne.json
index 72c4c8537..9bc5c0bfa 100644
--- a/locales/ne.json
+++ b/locales/ne.json
@@ -1,3 +1,3 @@
{
"password_too_simple_1": "पासवर्ड कम्तिमा characters अक्षर लामो हुनु आवश्यक छ"
-}
+}
\ No newline at end of file
diff --git a/locales/nl.json b/locales/nl.json
index dfee556b2..5e612fc77 100644
--- a/locales/nl.json
+++ b/locales/nl.json
@@ -1,30 +1,28 @@
{
- "action_invalid": "Ongeldige actie '{action:s}'",
+ "action_invalid": "Ongeldige actie '{action}'",
"admin_password": "Administrator wachtwoord",
"admin_password_changed": "Het administratie wachtwoord werd gewijzigd",
- "app_already_installed": "{app:s} is al geïnstalleerd",
- "app_argument_invalid": "'{name:s}' bevat ongeldige waarde: {error:s}",
- "app_argument_required": "Het '{name:s}' moet ingevuld worden",
+ "app_already_installed": "{app} is al geïnstalleerd",
+ "app_argument_invalid": "Kies een geldige waarde voor '{name}': {error}",
+ "app_argument_required": "Het '{name}' moet ingevuld worden",
"app_extraction_failed": "Kan installatiebestanden niet uitpakken",
"app_id_invalid": "Ongeldige app-id",
- "app_install_files_invalid": "Ongeldige installatiebestanden",
+ "app_install_files_invalid": "Deze bestanden kunnen niet worden geïnstalleerd",
"app_manifest_invalid": "Ongeldig app-manifest",
- "app_not_installed": "{app:s} is niet geïnstalleerd",
- "app_removed": "{app:s} succesvol verwijderd",
- "app_sources_fetch_failed": "Kan bronbestanden niet ophalen",
+ "app_not_installed": "{app} is niet geïnstalleerd",
+ "app_removed": "{app} succesvol verwijderd",
+ "app_sources_fetch_failed": "Kan bronbestanden niet ophalen, klopt de URL?",
"app_unknown": "Onbekende app",
- "app_upgrade_failed": "Kan app {app:s} niet updaten",
- "app_upgraded": "{app:s} succesvol geüpgraded",
- "ask_email": "Email-adres",
+ "app_upgrade_failed": "Kan app {app} niet updaten",
+ "app_upgraded": "{app} succesvol geüpgraded",
"ask_firstname": "Voornaam",
"ask_lastname": "Achternaam",
"ask_new_admin_password": "Nieuw administratorwachtwoord",
"ask_password": "Wachtwoord",
"backup_archive_name_exists": "Een backuparchief met dezelfde naam bestaat al",
"backup_cleaning_failed": "Kan tijdelijke backup map niet leeg maken",
- "backup_invalid_archive": "Ongeldig backup archief",
"backup_output_directory_not_empty": "Doelmap is niet leeg",
- "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app:s} bij te werken",
+ "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app} bij te werken",
"domain_cert_gen_failed": "Kan certificaat niet genereren",
"domain_created": "Domein succesvol aangemaakt",
"domain_creation_failed": "Kan domein niet aanmaken",
@@ -34,38 +32,32 @@
"domain_dyndns_root_unknown": "Onbekend DynDNS root domein",
"domain_exists": "Domein bestaat al",
"domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijdert",
- "domain_unknown": "Onbekend domein",
"done": "Voltooid",
"downloading": "Downloaden...",
- "dyndns_cron_remove_failed": "De cron-job voor DynDNS kon niet worden verwijderd",
"dyndns_ip_update_failed": "Kan het IP adres niet updaten bij DynDNS",
"dyndns_ip_updated": "IP adres is aangepast bij DynDNS",
"dyndns_key_generating": "DNS sleutel word aangemaakt, wacht een moment...",
"dyndns_unavailable": "DynDNS subdomein is niet beschikbaar",
- "executing_script": "Script uitvoeren...",
"extracting": "Uitpakken...",
"installation_complete": "Installatie voltooid",
- "installation_failed": "Installatie gefaald",
- "ldap_initialized": "LDAP is klaar voor gebruik",
- "mail_alias_remove_failed": "Kan mail-alias '{mail:s}' niet verwijderen",
- "no_internet_connection": "Server is niet verbonden met het internet",
+ "mail_alias_remove_failed": "Kan mail-alias '{mail}' niet verwijderen",
"pattern_email": "Moet een geldig emailadres bevatten (bv. abc@example.org)",
"pattern_mailbox_quota": "Mailbox quota moet een waarde bevatten met b/k/M/G/T erachter of 0 om geen quota in te stellen",
"pattern_password": "Wachtwoord moet tenminste 3 karakters lang zijn",
- "port_already_closed": "Poort {port:d} is al gesloten voor {ip_version:s} verbindingen",
- "port_already_opened": "Poort {port:d} is al open voor {ip_version:s} verbindingen",
- "restore_app_failed": "De app '{app:s}' kon niet worden terug gezet",
- "restore_hook_unavailable": "De herstel-hook '{part:s}' is niet beschikbaar op dit systeem",
- "service_add_failed": "Kan service '{service:s}' niet toevoegen",
- "service_already_started": "Service '{service:s}' draait al",
- "service_cmd_exec_failed": "Kan '{command:s}' niet uitvoeren",
- "service_disabled": "Service '{service:s}' is uitgeschakeld",
- "service_remove_failed": "Kan service '{service:s}' niet verwijderen",
+ "port_already_closed": "Poort {port} is al gesloten voor {ip_version} verbindingen",
+ "port_already_opened": "Poort {port} is al open voor {ip_version} verbindingen",
+ "app_restore_failed": "De app '{app}' kon niet worden terug gezet: {error}",
+ "restore_hook_unavailable": "De herstel-hook '{part}' is niet beschikbaar op dit systeem",
+ "service_add_failed": "Kan service '{service}' niet toevoegen",
+ "service_already_started": "Service '{service}' draait al",
+ "service_cmd_exec_failed": "Kan '{command}' niet uitvoeren",
+ "service_disabled": "Service '{service}' is uitgeschakeld",
+ "service_remove_failed": "Kan service '{service}' niet verwijderen",
"service_removed": "Service werd verwijderd",
- "service_stop_failed": "Kan service '{service:s}' niet stoppen",
- "service_unknown": "De service '{service:s}' bestaat niet",
+ "service_stop_failed": "Kan service '{service}' niet stoppen",
+ "service_unknown": "De service '{service}' bestaat niet",
"unexpected_error": "Er is een onbekende fout opgetreden",
- "unrestore_app": "App '{app:s}' wordt niet teruggezet",
+ "unrestore_app": "App '{app}' wordt niet teruggezet",
"updating_apt_cache": "Lijst van beschikbare pakketten wordt bijgewerkt...",
"upgrade_complete": "Upgrade voltooid",
"upgrading_packages": "Pakketten worden geüpdate...",
@@ -75,31 +67,50 @@
"upnp_port_open_failed": "Kan UPnP poorten niet openen",
"user_deleted": "Gebruiker werd verwijderd",
"user_home_creation_failed": "Kan de map voor deze gebruiker niet aanmaken",
- "user_unknown": "Gebruikersnaam {user:s} is onbekend",
+ "user_unknown": "Gebruikersnaam {user} is onbekend",
"user_update_failed": "Kan gebruiker niet bijwerken",
"yunohost_configured": "YunoHost configuratie is OK",
"admin_password_change_failed": "Wachtwoord kan niet veranderd worden",
- "app_argument_choice_invalid": "Ongeldige keuze voor argument '{name:s}'. Het moet een van de volgende keuzes zijn {choices:s}",
- "app_not_correctly_installed": "{app:s} schijnt niet juist geïnstalleerd te zijn",
- "app_not_properly_removed": "{app:s} werd niet volledig verwijderd",
- "app_requirements_checking": "Controleer noodzakelijke pakketten...",
+ "app_argument_choice_invalid": "Ongeldige keuze voor argument '{name}'. Het moet een van de volgende keuzes zijn {choices}",
+ "app_not_correctly_installed": "{app} schijnt niet juist geïnstalleerd te zijn",
+ "app_not_properly_removed": "{app} werd niet volledig verwijderd",
+ "app_requirements_checking": "Noodzakelijke pakketten voor {app} aan het controleren...",
"app_requirements_unmeet": "Er wordt niet aan de aanvorderingen voldaan, het pakket {pkgname} ({version}) moet {spec} zijn",
"app_unsupported_remote_type": "Niet ondersteund besturings type voor de app",
"ask_main_domain": "Hoofd-domein",
- "backup_app_failed": "Kon geen backup voor app '{app:s}' aanmaken",
- "backup_archive_app_not_found": "App '{app:s}' kon niet in het backup archief gevonden worden",
- "backup_archive_broken_link": "Het backup archief kon niet geopend worden (Ongeldig verwijs naar {path:s})",
- "backup_archive_name_unknown": "Onbekend lokaal backup archief namens '{name:s}' gevonden",
+ "backup_app_failed": "Kon geen backup voor app '{app}' aanmaken",
+ "backup_archive_app_not_found": "App '{app}' kon niet in het backup archief gevonden worden",
+ "backup_archive_broken_link": "Het backup archief kon niet geopend worden (Ongeldig verwijs naar {path})",
+ "backup_archive_name_unknown": "Onbekend lokaal backup archief namens '{name}' gevonden",
"backup_archive_open_failed": "Kan het backup archief niet openen",
"backup_created": "Backup aangemaakt",
"backup_creation_failed": "Aanmaken van backup mislukt",
- "backup_delete_error": "Kon pad '{path:s}' niet verwijderen",
+ "backup_delete_error": "Kon pad '{path}' niet verwijderen",
"backup_deleted": "Backup werd verwijderd",
- "backup_hook_unknown": "backup hook '{hook:s}' onbekend",
+ "backup_hook_unknown": "backup hook '{hook}' onbekend",
"backup_nothings_done": "Niets om op te slaan",
"password_too_simple_1": "Het wachtwoord moet minimaal 8 tekens lang zijn",
"already_up_to_date": "Er is niets te doen, alles is al up-to-date.",
"admin_password_too_long": "Gelieve een wachtwoord te kiezen met minder dan 127 karakters",
"app_action_cannot_be_ran_because_required_services_down": "De volgende diensten moeten actief zijn om deze actie uit te voeren: {services}. Probeer om deze te herstarten om verder te gaan (en om eventueel te onderzoeken waarom ze niet werken).",
- "aborting": "Annulatie."
+ "aborting": "Annulatie.",
+ "app_upgrade_app_name": "Bezig {app} te upgraden...",
+ "app_make_default_location_already_used": "Kan '{app}' niet de standaardapp maken op het domein, '{domain}' wordt al gebruikt door '{other_app}'",
+ "app_install_failed": "Kan {app} niet installeren: {error}",
+ "app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie...",
+ "app_manifest_install_ask_domain": "Kies het domein waar deze app op geïnstalleerd moet worden",
+ "app_manifest_install_ask_path": "Kies het pad waar deze app geïnstalleerd moet worden",
+ "app_manifest_install_ask_admin": "Kies een administrator voor deze app",
+ "app_change_url_success": "{app} URL is nu {domain}{path}",
+ "app_full_domain_unavailable": "Sorry, deze app moet op haar eigen domein geïnstalleerd worden, maar andere apps zijn al geïnstalleerd op het domein '{domain}'. U kunt wel een subdomein aan deze app toewijden.",
+ "app_install_script_failed": "Er is een fout opgetreden in het installatiescript van de app",
+ "app_location_unavailable": "Deze URL is niet beschikbaar of is in conflict met de al geïnstalleerde app(s):\n{apps}",
+ "app_manifest_install_ask_password": "Kies een administratiewachtwoord voor deze app",
+ "app_manifest_install_ask_is_public": "Moet deze app zichtbaar zijn voor anomieme bezoekers?",
+ "app_not_upgraded": "De app '{failed_app}' kon niet upgraden en daardoor zijn de upgrades van de volgende apps geannuleerd: {apps}",
+ "app_start_install": "{app} installeren...",
+ "app_start_remove": "{app} verwijderen...",
+ "app_start_backup": "Bestanden aan het verzamelen voor de backup van {app}...",
+ "app_start_restore": "{app} herstellen...",
+ "app_upgrade_several_apps": "De volgende apps zullen worden geüpgraded: {apps}"
}
\ No newline at end of file
diff --git a/locales/oc.json b/locales/oc.json
index 07d841579..a2a5bfe31 100644
--- a/locales/oc.json
+++ b/locales/oc.json
@@ -2,82 +2,77 @@
"admin_password": "Senhal d’administracion",
"admin_password_change_failed": "Impossible de cambiar lo senhal",
"admin_password_changed": "Lo senhal d’administracion es ben estat cambiat",
- "app_already_installed": "{app:s} es ja installat",
- "app_already_up_to_date": "{app:s} es ja a jorn",
+ "app_already_installed": "{app} es ja installat",
+ "app_already_up_to_date": "{app} es ja a jorn",
"installation_complete": "Installacion acabada",
"app_id_invalid": "ID d’aplicacion incorrècte",
"app_install_files_invalid": "Installacion impossibla d’aquestes fichièrs",
- "app_not_correctly_installed": "{app:s} sembla pas ben installat",
- "app_not_installed": "Impossible de trobar l’aplicacion {app:s} dins la lista de las aplicacions installadas : {all_apps}",
- "app_not_properly_removed": "{app:s} es pas estat corrèctament suprimit",
- "app_removed": "{app:s} es estada suprimida",
+ "app_not_correctly_installed": "{app} sembla pas ben installat",
+ "app_not_installed": "Impossible de trobar l’aplicacion {app} dins la lista de las aplicacions installadas : {all_apps}",
+ "app_not_properly_removed": "{app} es pas estat corrèctament suprimit",
+ "app_removed": "{app} es estada suprimida",
"app_unknown": "Aplicacion desconeguda",
"app_upgrade_app_name": "Actualizacion de l’aplicacion {app}...",
- "app_upgrade_failed": "Impossible d’actualizar {app:s} : {error}",
+ "app_upgrade_failed": "Impossible d’actualizar {app} : {error}",
"app_upgrade_some_app_failed": "D’aplicacions se pòdon pas actualizar",
- "app_upgraded": "{app:s} es estada actualizada",
- "ask_email": "Adreça de corrièl",
+ "app_upgraded": "{app} es estada actualizada",
"ask_firstname": "Prenom",
"ask_lastname": "Nom",
"ask_main_domain": "Domeni màger",
"ask_new_admin_password": "Nòu senhal administrator",
"ask_password": "Senhal",
- "backup_app_failed": "Impossible de salvagardar l’aplicacion « {app:s} »",
+ "backup_app_failed": "Impossible de salvagardar l’aplicacion « {app} »",
"backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda...",
"backup_applying_method_tar": "Creacion de l’archiu TAR de la salvagarda...",
"backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja.",
- "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut",
- "action_invalid": "Accion « {action:s} » incorrècta",
- "app_argument_choice_invalid": "Utilizatz una de las opcions « {choices:s} » per l’argument « {name:s} »",
- "app_argument_invalid": "Causissètz una valor invalida pel paramètre « {name:s} » : {error:s}",
- "app_argument_required": "Lo paramètre « {name:s} » es requesit",
- "app_change_url_failed_nginx_reload": "Reaviada de NGINX impossibla. Vaquí la sortida de « nginx -t » :\n{nginx_errors:s}",
- "app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain:s}{path:s}, pas res a far.",
- "app_change_url_success": "L’URL de l’aplicacion {app:s} es ara {domain:s}{path:s}",
+ "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name} » es desconegut",
+ "action_invalid": "Accion « {action} » incorrècta",
+ "app_argument_choice_invalid": "Utilizatz una de las opcions « {choices} » per l’argument « {name} »",
+ "app_argument_invalid": "Causissètz una valor invalida pel paramètre « {name} » : {error}",
+ "app_argument_required": "Lo paramètre « {name} » es requesit",
+ "app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain}{path}, pas res a far.",
+ "app_change_url_success": "L’URL de l’aplicacion {app} es ara {domain}{path}",
"app_extraction_failed": "Extraccion dels fichièrs d’installacion impossibla",
"app_manifest_invalid": "I a quicòm que truca amb lo manifest de l’aplicacion : {error}",
"app_requirements_checking": "Verificacion dels paquets requesits per {app}...",
"app_sources_fetch_failed": "Recuperacion dels fichièrs fonts impossibla, l’URL es corrècta ?",
"app_unsupported_remote_type": "Lo tipe alonhat utilizat per l’aplicacion es pas suportat",
- "backup_archive_app_not_found": "L’aplicacion « {app:s} » es pas estada trobada dins l’archiu de la salvagarda",
- "backup_archive_broken_link": "Impossible d’accedir a l’archiu de salvagarda (ligam invalid cap a {path:s})",
+ "backup_archive_app_not_found": "L’aplicacion « {app} » es pas estada trobada dins l’archiu de la salvagarda",
+ "backup_archive_broken_link": "Impossible d’accedir a l’archiu de salvagarda (ligam invalid cap a {path})",
"backup_archive_open_failed": "Impossible de dobrir l’archiu de salvagarda",
- "backup_archive_system_part_not_available": "La part « {part:s} » del sistèma es pas disponibla dins aquesta salvagarda",
+ "backup_archive_system_part_not_available": "La part « {part} » del sistèma es pas disponibla dins aquesta salvagarda",
"backup_cleaning_failed": "Impossible de netejar lo repertòri temporari de salvagarda",
- "backup_copying_to_organize_the_archive": "Còpia de {size:s} Mio per organizar l’archiu",
+ "backup_copying_to_organize_the_archive": "Còpia de {size} Mio per organizar l’archiu",
"backup_created": "Salvagarda acabada",
"backup_creation_failed": "Creacion impossibla de l’archiu de salvagarda",
"app_already_installed_cant_change_url": "Aquesta aplicacion es ja installada. Aquesta foncion pòt pas simplament cambiar l’URL. Agachatz « app changeurl » s’es disponible.",
- "app_change_url_no_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, benlèu que vos cal l’actualizar.",
+ "app_change_url_no_script": "L’aplicacion {app_name} pren pas en compte lo cambiament d’URL, benlèu que vos cal l’actualizar.",
"app_make_default_location_already_used": "Impossible de configurar l’aplicacion « {app} » per defaut pel domeni {domain} perque es ja utilizat per l’aplicacion {other_app}",
- "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta :\n{apps:s}",
- "backup_delete_error": "Supression impossibla de « {path:s} »",
+ "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta :\n{apps}",
+ "backup_delete_error": "Supression impossibla de « {path} »",
"backup_deleted": "La salvagarda es estada suprimida",
- "backup_hook_unknown": "Script de salvagarda « {hook:s} » desconegut",
- "backup_invalid_archive": "Aquò es pas un archiu de salvagarda",
- "backup_method_borg_finished": "La salvagarda dins Borg es acabada",
+ "backup_hook_unknown": "Script de salvagarda « {hook} » desconegut",
"backup_method_copy_finished": "La còpia de salvagarda es acabada",
"backup_method_tar_finished": "L’archiu TAR de la salvagarda es estat creat",
"backup_output_directory_not_empty": "Devètz causir un dorsièr de sortida void",
"backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda",
"backup_running_hooks": "Execucion dels scripts de salvagarda...",
- "backup_system_part_failed": "Impossible de salvagardar la part « {part:s} » del sistèma",
+ "backup_system_part_failed": "Impossible de salvagardar la part « {part} » del sistèma",
"app_requirements_unmeet": "Las condicions requesidas per {app} son pas complidas, lo paquet {pkgname} ({version}) deu èsser {spec}",
"backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat",
- "backup_applying_method_custom": "Crida del metòde de salvagarda personalizat « {method:s} »...",
- "backup_borg_not_implemented": "Lo metòde de salvagarda Bord es pas encara implementat",
- "backup_couldnt_bind": "Impossible de ligar {src:s} amb {dest:s}.",
+ "backup_applying_method_custom": "Crida del metòde de salvagarda personalizat « {method} »...",
+ "backup_couldnt_bind": "Impossible de ligar {src} amb {dest}.",
"backup_csv_addition_failed": "Impossible d’ajustar de fichièrs a la salvagarda dins lo fichièr CSV",
"backup_custom_backup_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « backup »",
"backup_custom_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « mount »",
- "backup_method_custom_finished": "Lo metòde de salvagarda personalizat « {method:s} » es acabat",
+ "backup_method_custom_finished": "Lo metòde de salvagarda personalizat « {method} » es acabat",
"backup_nothings_done": "I a pas res de salvagardar",
"backup_unable_to_organize_files": "Impossible d’organizar los fichièrs dins l’archiu amb lo metòde rapid",
- "service_stopped": "Lo servici « {service:s} » es estat arrestat",
- "service_unknown": "Servici « {service:s} » desconegut",
- "unbackup_app": "L’aplicacion « {app:s} » serà pas salvagardada",
+ "service_stopped": "Lo servici « {service} » es estat arrestat",
+ "service_unknown": "Servici « {service} » desconegut",
+ "unbackup_app": "L’aplicacion « {app} » serà pas salvagardada",
"unlimit": "Cap de quòta",
- "unrestore_app": "L’aplicacion « {app:s} » serà pas restaurada",
+ "unrestore_app": "L’aplicacion « {app} » serà pas restaurada",
"upnp_dev_not_found": "Cap de periferic compatible UPnP pas trobat",
"upnp_disabled": "UPnP es desactivat",
"upnp_enabled": "UPnP es activat",
@@ -85,26 +80,24 @@
"yunohost_already_installed": "YunoHost es ja installat",
"yunohost_configured": "YunoHost es estat configurat",
"yunohost_installing": "Installacion de YunoHost…",
- "backup_applying_method_borg": "Mandadís de totes los fichièrs a la salvagarda dins lo repertòri borg-backup…",
"backup_csv_creation_failed": "Creacion impossibla del fichièr CSV necessari a las operacions futuras de restauracion",
- "backup_output_symlink_dir_broken": "Vòstre repertòri d’archiu « {path:s} » es un ligam simbolic copat. Saique oblidèretz de re/montar o de connectar supòrt.",
- "backup_with_no_backup_script_for_app": "L’aplicacion {app:s} a pas cap de script de salvagarda. I fasèm pas cas.",
- "backup_with_no_restore_script_for_app": "{app:s} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.",
- "certmanager_acme_not_configured_for_domain": "Lo certificat pel domeni {domain:s} sembla pas corrèctament installat. Mercés de lançar d’en primièr « cert-install » per aqueste domeni.",
- "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain:s} es pas provesit per Let’s Encrypt. Impossible de lo renovar automaticament !",
- "certmanager_attempt_to_renew_valid_cert": "Lo certificat pel domeni {domain:s} es a man d’expirar ! (Podètz utilizar --force se sabètz çò que fasètz)",
- "certmanager_cannot_read_cert": "Quicòm a trucat en ensajar de dobrir lo certificat actual pel domeni {domain:s} (fichièr : {file:s}), rason : {reason:s}",
- "certmanager_cert_install_success": "Lo certificat Let’s Encrypt es ara installat pel domeni « {domain:s} »",
- "certmanager_cert_install_success_selfsigned": "Lo certificat auto-signat es ara installat pel domeni « {domain:s} »",
+ "backup_output_symlink_dir_broken": "Vòstre repertòri d’archiu « {path} » es un ligam simbolic copat. Saique oblidèretz de re/montar o de connectar supòrt.",
+ "backup_with_no_backup_script_for_app": "L’aplicacion {app} a pas cap de script de salvagarda. I fasèm pas cas.",
+ "backup_with_no_restore_script_for_app": "{app} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.",
+ "certmanager_acme_not_configured_for_domain": "Lo certificat pel domeni {domain} sembla pas corrèctament installat. Mercés de lançar d’en primièr « cert-install » per aqueste domeni.",
+ "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain} es pas provesit per Let’s Encrypt. Impossible de lo renovar automaticament !",
+ "certmanager_attempt_to_renew_valid_cert": "Lo certificat pel domeni {domain} es a man d’expirar ! (Podètz utilizar --force se sabètz çò que fasètz)",
+ "certmanager_cannot_read_cert": "Quicòm a trucat en ensajar de dobrir lo certificat actual pel domeni {domain} (fichièr : {file}), rason : {reason}",
+ "certmanager_cert_install_success": "Lo certificat Let’s Encrypt es ara installat pel domeni « {domain} »",
+ "certmanager_cert_install_success_selfsigned": "Lo certificat auto-signat es ara installat pel domeni « {domain} »",
"certmanager_cert_signing_failed": "Signatura impossibla del nòu certificat",
- "certmanager_domain_cert_not_selfsigned": "Lo certificat pel domeni {domain:s} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utilizatz « --force » per o far)",
- "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » pel domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz çò que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)",
- "certmanager_domain_http_not_working": "Sembla que lo domeni {domain:s} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e NGINK son corrèctas",
- "certmanager_domain_unknown": "Domeni desconegut « {domain:s} »",
- "certmanager_no_cert_file": "Lectura impossibla del fichièr del certificat pel domeni {domain:s} (fichièr : {file:s})",
- "certmanager_self_ca_conf_file_not_found": "Impossible de trobar lo fichièr de configuracion per l’autoritat del certificat auto-signat (fichièr : {file:s})",
- "certmanager_unable_to_parse_self_CA_name": "Analisi impossibla del nom de l’autoritat del certificat auto-signat (fichièr : {file:s})",
- "custom_app_url_required": "Cal que donetz una URL per actualizar vòstra aplicacion personalizada {app:s}",
+ "certmanager_domain_cert_not_selfsigned": "Lo certificat pel domeni {domain} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utilizatz « --force » per o far)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » pel domeni {domain} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz çò que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)",
+ "certmanager_domain_http_not_working": "Sembla que lo domeni {domain} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e NGINK son corrèctas",
+ "certmanager_no_cert_file": "Lectura impossibla del fichièr del certificat pel domeni {domain} (fichièr : {file})",
+ "certmanager_self_ca_conf_file_not_found": "Impossible de trobar lo fichièr de configuracion per l’autoritat del certificat auto-signat (fichièr : {file})",
+ "certmanager_unable_to_parse_self_CA_name": "Analisi impossibla del nom de l’autoritat del certificat auto-signat (fichièr : {file})",
+ "custom_app_url_required": "Cal que donetz una URL per actualizar vòstra aplicacion personalizada {app}",
"domain_cannot_remove_main": "Impossible de levar lo domeni màger. Definissètz un novèl domeni màger d’en primièr",
"domain_cert_gen_failed": "Generacion del certificat impossibla",
"domain_created": "Domeni creat",
@@ -114,51 +107,31 @@
"domain_dyndns_root_unknown": "Domeni DynDNS màger desconegut",
"domain_exists": "Lo domeni existís ja",
"domain_hostname_failed": "Fracàs de la creacion d’un nòu nom d’òst. Aquò poirà provocar de problèmas mai tard (mas es pas segur… benlèu que coparà pas res).",
- "domain_unknown": "Domeni desconegut",
"domains_available": "Domenis disponibles :",
"done": "Acabat",
"downloading": "Telecargament…",
- "dyndns_could_not_check_provide": "Impossible de verificar se {provider:s} pòt provesir {domain:s}.",
- "dyndns_cron_installed": "Tasca cron pel domeni DynDNS creada",
- "dyndns_cron_remove_failed": "Impossible de levar la tasca cron pel domeni DynDNS : {error}",
- "dyndns_cron_removed": "Tasca cron pel domeni DynDNS levada",
+ "dyndns_could_not_check_provide": "Impossible de verificar se {provider} pòt provesir {domain}.",
"dyndns_ip_update_failed": "Impossible d’actualizar l’adreça IP sul domeni DynDNS",
"dyndns_ip_updated": "Vòstra adreça IP actualizada pel domeni DynDNS",
"dyndns_key_generating": "La clau DNS es a se generar… pòt trigar una estona.",
"dyndns_key_not_found": "Clau DNS introbabla pel domeni",
"dyndns_no_domain_registered": "Cap de domeni pas enregistrat amb DynDNS",
"dyndns_registered": "Domeni DynDNS enregistrat",
- "dyndns_registration_failed": "Enregistrament del domeni DynDNS impossible : {error:s}",
- "dyndns_domain_not_provided": "Lo provesidor DynDNS {provider:s} pòt pas fornir lo domeni {domain:s}.",
- "dyndns_unavailable": "Lo domeni {domain:s} es pas disponible.",
+ "dyndns_registration_failed": "Enregistrament del domeni DynDNS impossible : {error}",
+ "dyndns_domain_not_provided": "Lo provesidor DynDNS {provider} pòt pas fornir lo domeni {domain}.",
+ "dyndns_unavailable": "Lo domeni {domain} es pas disponible.",
"extracting": "Extraccion…",
- "field_invalid": "Camp incorrècte : « {:s} »",
- "global_settings_cant_open_settings": "Fracàs de la dobertura del fichièr de configuracion, rason : {reason:s}",
- "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en executant « yunohost settings list »",
- "global_settings_reset_success": "Configuracion precedenta ara salvagarda dins {path:s}",
- "global_settings_setting_example_bool": "Exemple d’opcion booleana",
- "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key:s}, apartada e salvagardada dins /etc/yunohost/settings-unknown.json",
- "installation_failed": "Quicòm a trucat e l’installacion a pas reüssit",
- "ldap_initialized": "L’annuari LDAP es inicializat",
+ "field_invalid": "Camp incorrècte : « {} »",
+ "global_settings_cant_open_settings": "Fracàs de la dobertura del fichièr de configuracion, rason : {reason}",
+ "global_settings_key_doesnt_exists": "La clau « {settings_key} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en executant « yunohost settings list »",
+ "global_settings_reset_success": "Configuracion precedenta ara salvagarda dins {path}",
+ "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key}, apartada e salvagardada dins /etc/yunohost/settings-unknown.json",
"main_domain_change_failed": "Modificacion impossibla del domeni màger",
"main_domain_changed": "Lo domeni màger es estat modificat",
- "migrate_tsig_end": "La migracion cap a HMAC-SHA512 es acabada",
- "migrate_tsig_wait_2": "2 minutas…",
- "migrate_tsig_wait_3": "1 minuta…",
- "migrate_tsig_wait_4": "30 segondas…",
- "migration_description_0002_migrate_to_tsig_sha256": "Melhora la seguretat de DynDNS TSIG en utilizar SHA512 allòc de MD5",
- "migration_description_0003_migrate_to_stretch": "Mesa a nivèl del sistèma cap a Debian Stretch e YunoHost 3.0",
- "migration_0003_start": "Aviada de la migracion cap a Stretech. Los jornals seràn disponibles dins {logfile}.",
- "migration_0003_patching_sources_list": "Petaçatge de sources.lists…",
- "migration_0003_main_upgrade": "Aviada de la mesa a nivèl màger…",
- "migration_0003_fail2ban_upgrade": "Aviada de la mesa a nivèl de Fail2Ban…",
- "migration_0003_not_jessie": "La distribucion Debian actuala es pas Jessie !",
"migrations_cant_reach_migration_file": "Impossible d’accedir als fichièrs de migracion amb lo camin %s",
"migrations_list_conflict_pending_done": "Podètz pas utilizar --previous e --done a l’encòp.",
"migrations_loading_migration": "Cargament de la migracion {id}…",
"migrations_no_migrations_to_run": "Cap de migracion de lançar",
- "no_internet_connection": "Lo servidor es pas connectat a Internet",
- "package_unknown": "Paquet « {pkgname} » desconegut",
"packages_upgrade_failed": "Actualizacion de totes los paquets impossibla",
"pattern_domain": "Deu èsser un nom de domeni valid (ex : mon-domeni.org)",
"pattern_email": "Deu èsser una adreça electronica valida (ex : escais@domeni.org)",
@@ -166,57 +139,42 @@
"pattern_lastname": "Deu èsser un nom valid",
"pattern_password": "Deu conténer almens 3 caractèrs",
"pattern_port_or_range": "Deu èsser un numèro de pòrt valid (ex : 0-65535) o un interval de pòrt (ex : 100:200)",
- "pattern_positive_number": "Deu èsser un nombre positiu",
- "port_already_closed": "Lo pòrt {port:d} es ja tampat per las connexions {ip_version:s}",
- "port_already_opened": "Lo pòrt {port:d} es ja dubèrt per las connexions {ip_version:s}",
- "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »",
- "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »",
- "backup_ask_for_copying_if_needed": "Volètz far una salvagarda en utilizant {size:s} Mo temporàriament ? (Aqueste biais de far es emplegat perque unes fichièrs an pas pogut èsser preparats amb un metòde mai eficaç.)",
+ "port_already_closed": "Lo pòrt {port} es ja tampat per las connexions {ip_version}",
+ "port_already_opened": "Lo pòrt {port} es ja dubèrt per las connexions {ip_version}",
+ "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app} »",
+ "app_restore_failed": "Impossible de restaurar l’aplicacion « {app} »: {error}",
+ "backup_ask_for_copying_if_needed": "Volètz far una salvagarda en utilizant {size} Mo temporàriament ? (Aqueste biais de far es emplegat perque unes fichièrs an pas pogut èsser preparats amb un metòde mai eficaç.)",
"yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »",
"backup_output_directory_forbidden": "Causissètz un repertòri de destinacion deferent. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
- "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain:s} ! (Utilizatz --force per cortcircuitar)",
- "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni « {domain:s} »",
- "certmanager_certificate_fetching_or_enabling_failed": "Sembla qu’utilizar lo nòu certificat per {domain:s} fonciona pas...",
- "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion NGINX {filepath:s} es en conflicte e deu èsser levat d’en primièr",
- "certmanager_couldnt_fetch_intermediate_cert": "Expiracion del relambi pendent l’ensag de recuperacion del certificat intermediari dins de Let’s Encrypt. L’installacion / lo renovèlament es estat interromput - tornatz ensajar mai tard.",
- "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let’S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)",
- "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs",
- "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafuòc/router amont de vòstre servidor es mal configurat.",
+ "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain} ! (Utilizatz --force per cortcircuitar)",
+ "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni « {domain} »",
+ "certmanager_certificate_fetching_or_enabling_failed": "Sembla qu’utilizar lo nòu certificat per {domain} fonciona pas...",
+ "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs",
"domain_dns_conf_is_just_a_recommendation": "Aqueste pagina mòstra la configuracion *recomandada*. Non configura *pas* lo DNS per vos. Sètz responsable de la configuracion de vòstra zòna DNS en çò de vòstre registrar DNS amb aquesta recomandacion.",
"domain_dyndns_already_subscribed": "Avètz ja soscrich a un domeni DynDNS",
"domain_uninstall_app_first": "Una o mantuna aplicacions son installadas sus aqueste domeni. Mercés de las desinstallar d’en primièr abans de suprimir aqueste domeni",
"firewall_reload_failed": "Impossible de recargar lo parafuòc",
"firewall_reloaded": "Parafuòc recargat",
"firewall_rules_cmd_failed": "Unas règlas del parafuòc an fracassat. Per mai informacions, consultatz lo jornal.",
- "global_settings_bad_choice_for_enum": "La valor del paramètre {setting:s} es incorrècta. Recebut : {choice:s}, mas las opcions esperadas son : {available_choices:s}",
- "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting:s} es incorrècte, recebut : {received_type:s}, esperat {expected_type:s}",
- "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason:s}",
- "global_settings_setting_example_enum": "Exemple d’opcion de tipe enumeracion",
- "global_settings_setting_example_int": "Exemple d’opcion de tipe entièr",
- "global_settings_setting_example_string": "Exemple d’opcion de tipe cadena",
- "global_settings_unknown_type": "Situacion inesperada, la configuracion {setting:s} sembla d’aver lo tipe {unknown_type:s} mas es pas un tipe pres en carga pel sistèma.",
- "hook_exec_failed": "Fracàs de l’execucion del script : « {path:s} »",
- "hook_exec_not_terminated": "Lo escript « {path:s} » a pas acabat corrèctament",
+ "global_settings_bad_choice_for_enum": "La valor del paramètre {setting} es incorrècta. Recebut : {choice}, mas las opcions esperadas son : {available_choices}",
+ "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting} es incorrècte, recebut : {received_type}, esperat {expected_type}",
+ "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason}",
+ "global_settings_unknown_type": "Situacion inesperada, la configuracion {setting} sembla d’aver lo tipe {unknown_type} mas es pas un tipe pres en carga pel sistèma.",
+ "hook_exec_failed": "Fracàs de l’execucion del script : « {path} »",
+ "hook_exec_not_terminated": "Lo escript « {path} » a pas acabat corrèctament",
"hook_list_by_invalid": "La proprietat de tria de las accions es invalida",
- "hook_name_unknown": "Nom de script « {name:s} » desconegut",
- "ldap_init_failed_to_create_admin": "L’inicializacion de LDAP a pas pogut crear l’utilizaire admin",
- "mail_domain_unknown": "Lo domeni de corrièl « {domain:s} » es desconegut",
+ "hook_name_unknown": "Nom de script « {name} » desconegut",
+ "mail_domain_unknown": "Lo domeni de corrièl « {domain} » es desconegut",
"mailbox_used_space_dovecot_down": "Lo servici corrièl Dovecot deu èsser aviat, se volètz conéisser l’espaci ocupat per la messatjariá",
- "migrate_tsig_failed": "La migracion del domeni DynDNS {domain} cap a HMAC-SHA512 a pas capitat, anullacion de las modificacions. Error : {error_code} - {error}",
- "migrate_tsig_wait": "Esperem 3 minutas que lo servidor DynDNS prenga en compte la novèla clau…",
- "migrate_tsig_not_needed": "Sembla qu’utilizatz pas un domeni DynDNS, donc cap de migracion es pas necessària.",
- "migration_0003_yunohost_upgrade": "Aviada de la mesa a nivèl del paquet YunoHost… La migracion acabarà, mas l’actualizacion reala se realizarà tot bèl aprèp. Un còp acabada, poiretz vos reconnectar a l’administracion web.",
- "migration_0003_system_not_fully_up_to_date": "Lo sistèma es pas complètament a jorn. Mercés de lançar una mesa a jorn classica abans de començar la migracion per Stretch.",
- "migration_0003_modified_files": "Mercés de notar que los fichièrs seguents son estats detectats coma modificats manualament e poiràn èsser escafats a la fin de la mesa a nivèl : {manually_modified_files}",
- "service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}",
- "service_disabled": "Lo servici « {service:s} » es desactivat",
- "service_enable_failed": "Impossible d’activar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}",
- "service_enabled": "Lo servici « {service:s} » es activat",
- "service_remove_failed": "Impossible de levar lo servici « {service:s} »",
- "service_removed": "Lo servici « {service:s} » es estat levat",
- "service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}",
- "service_started": "Lo servici « {service:s} » es aviat",
- "service_stop_failed": "Impossible d’arrestar lo servici « {service:s} »↵\n\nJornals recents : {logs:s}",
+ "service_disable_failed": "Impossible de desactivar lo servici « {service} »↵\n↵\nJornals recents : {logs}",
+ "service_disabled": "Lo servici « {service} » es desactivat",
+ "service_enable_failed": "Impossible d’activar lo servici « {service} »↵\n↵\nJornals recents : {logs}",
+ "service_enabled": "Lo servici « {service} » es activat",
+ "service_remove_failed": "Impossible de levar lo servici « {service} »",
+ "service_removed": "Lo servici « {service} » es estat levat",
+ "service_start_failed": "Impossible d’aviar lo servici « {service} »↵\n↵\nJornals recents : {logs}",
+ "service_started": "Lo servici « {service} » es aviat",
+ "service_stop_failed": "Impossible d’arrestar lo servici « {service} »↵\n\nJornals recents : {logs}",
"ssowat_conf_generated": "La configuracion SSowat es generada",
"ssowat_conf_updated": "La configuracion SSOwat es estada actualizada",
"system_upgraded": "Lo sistèma es estat actualizat",
@@ -229,56 +187,45 @@
"user_deleted": "L’utilizaire es suprimit",
"user_deletion_failed": "Supression impossibla de l’utilizaire",
"user_home_creation_failed": "Creacion impossibla del repertòri personal a l’utilizaire",
- "user_unknown": "Utilizaire « {user:s} » desconegut",
+ "user_unknown": "Utilizaire « {user} » desconegut",
"user_update_failed": "Modificacion impossibla de l’utilizaire",
"user_updated": "L’utilizaire es estat modificat",
- "yunohost_ca_creation_failed": "Creacion impossibla de l’autoritat de certificacion",
- "yunohost_ca_creation_success": "L’autoritat de certificacion locala es creada.",
- "service_description_avahi-daemon": "permet d’aténher vòstre servidor via yunohost.local sus vòstre ret local",
"service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)",
"updating_apt_cache": "Actualizacion de la lista dels paquets disponibles…",
- "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers:s}",
- "service_add_failed": "Apondon impossible del servici « {service:s} »",
- "service_added": "Lo servici « {service:s} » es ajustat",
- "service_already_started": "Lo servici « {service:s} » es ja aviat",
- "service_already_stopped": "Lo servici « {service:s} » es ja arrestat",
+ "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers}",
+ "service_add_failed": "Apondon impossible del servici « {service} »",
+ "service_added": "Lo servici « {service} » es ajustat",
+ "service_already_started": "Lo servici « {service} » es ja aviat",
+ "service_already_stopped": "Lo servici « {service} » es ja arrestat",
"restore_cleaning_failed": "Impossible de netejar lo repertòri temporari de restauracion",
"restore_complete": "Restauracion acabada",
- "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers:s}",
+ "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers}",
"restore_extracting": "Extraccions dels fichièrs necessaris dins de l’archiu…",
"restore_failed": "Impossible de restaurar lo sistèma",
- "restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tanpauc dins l’archiu",
- "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)",
- "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)",
+ "restore_hook_unavailable": "Lo script de restauracion « {part} » es pas disponible sus vòstre sistèma e es pas tanpauc dins l’archiu",
+ "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space} octets, necessari : {needed_space} octets, marge de seguretat : {margin} octets)",
+ "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space} octets, necessari : {needed_space} octets, marge de seguretat : {margin} octets)",
"restore_nothings_done": "Res es pas estat restaurat",
"restore_removing_tmp_dir_failed": "Impossible de levar u ancian repertòri temporari",
- "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app:s} »…",
+ "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app} »…",
"restore_running_hooks": "Execucion dels scripts de restauracion…",
- "restore_system_part_failed": "Restauracion impossibla de la part « {part:s} » del sistèma",
+ "restore_system_part_failed": "Restauracion impossibla de la part « {part} » del sistèma",
"server_shutdown": "Lo servidor serà atudat",
- "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers:s}",
+ "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers}",
"server_reboot": "Lo servidor es per reaviar",
- "not_enough_disk_space": "Espaci disc insufisent sus « {path:s} »",
- "service_cmd_exec_failed": "Impossible d’executar la comanda « {command:s} »",
+ "not_enough_disk_space": "Espaci disc insufisent sus « {path} »",
+ "service_cmd_exec_failed": "Impossible d’executar la comanda « {command} »",
"service_description_mysql": "garda las donadas de las aplicacions (base de donadas SQL)",
"service_description_postfix": "emplegat per enviar e recebre de corrièls",
"service_description_slapd": "garda los utilizaires, domenis e lors informacions ligadas",
"service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)",
"service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma",
"service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis",
- "executing_command": "Execucion de la comanda « {command:s} »…",
- "executing_script": "Execucion del script « {script:s} »…",
- "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason:s}",
+ "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason}",
"ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion",
"iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion",
- "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail:s} »",
- "mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail:s} »",
- "migrate_tsig_start": "L’algorisme de generacion de claus es pas pro securizat per la signatura TSIG del domeni « {domain} », lançament de la migracion cap a un mai segur HMAC-SHA-512",
- "migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »",
- "migration_0003_restoring_origin_nginx_conf": "Vòstre fichièr /etc/nginx/nginx.conf es estat modificat manualament. La migracion reïnicializarà d’en primièr son estat origina… Lo fichièr precedent serà disponible coma {backup_dest}.",
- "migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log}…",
- "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas. Mai d’informacions a https://yunohost.org/backup ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.\n\nEn mai, lo pòrt per SMTP, utilizat pels clients de corrièls extèrns (coma Thunderbird o K9-Mail per exemple) foguèt cambiat de 465 (SSL/TLS) per 587 (STARTTLS). L’ancian pòrt 465 serà automaticament tampat e lo nòu pòrt 587 serà dobèrt dins lo parafuòc. Vosautres e vòstres utilizaires *auretz* d’adaptar la configuracion de vòstre client de corrièl segon aqueles cambiaments !",
- "migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}",
+ "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail} »",
+ "mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail} »",
"migrations_migration_has_failed": "La migracion {id} a pas capitat, abandon. Error : {exception}",
"migrations_skip_migration": "Passatge de la migracion {id}…",
"migrations_to_be_ran_manually": "La migracion {id} deu èsser lançada manualament. Mercés d’anar a Aisinas > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations run ».",
@@ -288,20 +235,17 @@
"service_description_fail2ban": "protegís contra los atacs brute-force e d’autres atacs venents d’Internet",
"service_description_metronome": "gerís los comptes de messatjariás instantanèas XMPP",
"service_description_nginx": "fornís o permet l’accès a totes los sites web albergats sus vòstre servidor",
- "service_description_nslcd": "gerís la connexion en linha de comanda dels utilizaires YunoHost",
"service_description_redis-server": "una basa de donadas especializada per un accès rapid a las donadas, las filas d’espèra e la comunicacion entre programas",
"service_description_rspamd": "filtra lo corrièl pas desirat e mai foncionalitats ligadas al corrièl",
"pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o 0 per desactivar la quòta",
- "backup_archive_writing_error": "Impossible d’ajustar los fichièrs « {source:s} » a la salvagarda (nomenats dins l’archiu « {dest:s} »)dins l’archiu comprimit « {archive:s} »",
+ "backup_archive_writing_error": "Impossible d’ajustar los fichièrs « {source} » a la salvagarda (nomenats dins l’archiu « {dest} »)dins l’archiu comprimit « {archive} »",
"backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit",
"backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas",
"pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses",
"experimental_feature": "Atencion : aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas l’utilizar levat que sapiatz çò que fasètz.",
- "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error:s}",
- "log_category_404": "La categoria de jornals d’audit « {category} » existís pas",
+ "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error}",
"log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}",
- "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name}{name} »",
- "backup_php5_to_php7_migration_may_fail": "Impossible de convertir vòstre archiu per prendre en carga PHP 7, la restauracion de vòstras aplicacions PHP pòt reüssir pas a restaurar vòstras aplicacions PHP (rason : {error:s})",
+ "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name} »",
"log_link_to_failed_log": "L’operacion « {desc} » a pas capitat ! Per obténer d’ajuda, mercés de fornir lo jornal complèt de l’operacion",
"log_help_to_get_failed_log": "L’operacion « {desc} » a pas reüssit ! Per obténer d’ajuda, mercés de partejar lo jornal d’audit complèt d’aquesta operacion en utilizant la comanda « yunohost log share {name} »",
"log_does_exists": "I a pas cap de jornal d’audit per l’operacion amb lo nom « {log} », utilizatz « yunohost log list » per veire totes los jornals d’operacion disponibles",
@@ -327,23 +271,14 @@
"log_user_delete": "Levar l’utilizaire « {} »",
"log_user_update": "Actualizar las informacions de l’utilizaire « {} »",
"log_domain_main_domain": "Far venir « {} » lo domeni màger",
- "log_tools_migrations_migrate_forward": "Migrar",
+ "log_tools_migrations_migrate_forward": "Executar las migracions",
"log_tools_postinstall": "Realizar la post installacion del servidor YunoHost",
"log_tools_upgrade": "Actualizacion dels paquets sistèma",
"log_tools_shutdown": "Atudar lo servidor",
"log_tools_reboot": "Reaviar lo servidor",
"mail_unavailable": "Aquesta adreça electronica es reservada e deu èsser automaticament atribuida al tot bèl just primièr utilizaire",
- "migration_description_0004_php5_to_php7_pools": "Tornar configurar lo pools PHP per utilizar PHP 7 allòc del 5",
- "migration_description_0005_postgresql_9p4_to_9p6": "Migracion de las basas de donadas de PostgreSQL9.4 cap a 9.6",
- "migration_0005_postgresql_94_not_installed": "PostgreSQL es pas installat sul sistèma. I a pas res per far.",
- "migration_0005_postgresql_96_not_installed": "Avèm trobat que Postgresql 9.4 es installat, mas cap de version de Postgresql 9.6 pas trobada !? Quicòm d’estranh a degut arribar a vòstre sistèma :( …",
- "migration_0005_not_enough_space": "I a pas pro d’espaci disponible sus {path} per lançar la migracion d’aquela passa :(.",
- "service_description_php7.0-fpm": "executa d’aplicacions escrichas en PHP amb nginx",
- "users_available": "Lista dels utilizaires disponibles :",
- "good_practices_about_admin_password": "Sètz per definir un nòu senhal per l’administracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far d’utilizar un senhal mai long qu’aquò (ex. una passafrasa) e/o d’utilizar mantun tipes de caractèrs (majuscula, minuscula, nombre e caractèrs especials).",
- "good_practices_about_user_password": "Sètz a mand de definir un nòu senhal d’utilizaire. Lo nòu senhal deu conténer almens 8 caractèrs, es de bon far d’utilizar un senhal mai long (es a dire una frasa de senhal) e/o utilizar mantuns tipes de caractèrs (majusculas, minusculas, nombres e caractèrs especials).",
- "migration_description_0006_sync_admin_and_root_passwords": "Sincronizar los senhals admin e root",
- "migration_0006_disclaimer": "Ara YunoHost s’espèra que los senhals admin e root sián sincronizats. En lançant aquesta migracion, vòstre senhal root serà remplaçat pel senhal admin.",
+ "good_practices_about_admin_password": "Sètz per definir un nòu senhal per l’administracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far d’utilizar un senhal mai long qu’aquò (ex. una passafrasa) e/o d’utilizar mantun tipe de caractèrs (majuscula, minuscula, nombre e caractèrs especials).",
+ "good_practices_about_user_password": "Sètz a mand de definir un nòu senhal d’utilizaire. Lo nòu senhal deu conténer almens 8 caractèrs, es de bon far d’utilizar un senhal mai long (es a dire una frasa de senhal) e/o utilizar mantun tipe de caractèrs (majusculas, minusculas, nombres e caractèrs especials).",
"password_listed": "Aqueste senhal es un dels mai utilizats al monde. Se vos plai utilizatz-ne un mai unic.",
"password_too_simple_1": "Lo senhal deu conténer almens 8 caractèrs",
"password_too_simple_2": "Lo senhal deu conténer almens 8 caractèrs e numbres, majusculas e minusculas",
@@ -361,34 +296,27 @@
"ask_new_path": "Nòu camin",
"backup_actually_backuping": "Creacion d’un archiu de seguretat a partir dels fichièrs recuperats...",
"backup_mount_archive_for_restore": "Preparacion de l’archiu per restauracion...",
- "dyndns_could_not_check_available": "Verificacion impossibla de la disponibilitat de {domain:s} sus {provider:s}.",
- "file_does_not_exist": "Lo camin {path:s} existís pas.",
+ "dyndns_could_not_check_available": "Verificacion impossibla de la disponibilitat de {domain} sus {provider}.",
+ "file_does_not_exist": "Lo camin {path} existís pas.",
"global_settings_setting_security_password_admin_strength": "Fòrça del senhal administrator",
"global_settings_setting_security_password_user_strength": "Fòrça del senhal utilizaire",
- "migration_description_0007_ssh_conf_managed_by_yunohost_step1": "La configuracion SSH serà gerada per YunoHost (etapa 1, automatica)",
- "migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Daissar YunoHost gerir la configuracion SSH (etapa 2, manuala)",
- "migration_0007_cancelled": "Impossible de melhorar lo biais de gerir la configuracion SSH.",
"root_password_replaced_by_admin_password": "Lo senhal root es estat remplaçat pel senhal administrator.",
- "service_restarted": "Lo servici '{service:s}' es estat reaviat",
+ "service_restarted": "Lo servici '{service}' es estat reaviat",
"admin_password_too_long": "Causissètz un senhal d’almens 127 caractèrs",
- "migration_0007_cannot_restart": "SSH pòt pas èsser reavit aprèp aver ensajat d’anullar la migracion numèro 6.",
- "service_reloaded": "Lo servici « {service:s} » es estat tornat cargar",
+ "service_reloaded": "Lo servici « {service} » es estat tornat cargar",
"already_up_to_date": "I a pas res a far ! Tot es ja a jorn !",
"app_action_cannot_be_ran_because_required_services_down": "Aquestas aplicacions necessitan d’èsser lançadas per poder executar aquesta accion : {services}. Abans de contunhar deuriatz ensajar de reaviar los servicis seguents (e tanben cercar perque son tombats en pana) : {services}",
- "confirm_app_install_warning": "Atencion : aquesta aplicacion fonciona mas non es pas ben integrada amb YunoHost. Unas foncionalitats coma l’autentificacion unica e la còpia de seguretat/restauracion pòdon èsser indisponiblas. volètz l’installar de totas manièras ? [{answers:s}] ",
- "confirm_app_install_danger": "PERILH ! Aquesta aplicacion es encara experimentala (autrament dich, fonciona pas) e es possible que còpe lo sistèma ! Deuriatz PAS l’installar se non sabètz çò que fasètz. Volètz vertadièrament córrer aqueste risc ? [{answers:s}]",
- "confirm_app_install_thirdparty": "ATENCION ! L’installacion d’aplicacions tèrças pòt comprometre l’integralitat e la seguretat del sistèma. Deuriatz PAS l’installar se non sabètz pas çò que fasètz. Volètz vertadièrament córrer aqueste risc ? [{answers:s}] ",
+ "confirm_app_install_warning": "Atencion : aquesta aplicacion fonciona mas non es pas ben integrada amb YunoHost. Unas foncionalitats coma l’autentificacion unica e la còpia de seguretat/restauracion pòdon èsser indisponiblas. volètz l’installar de totas manièras ? [{answers}] ",
+ "confirm_app_install_danger": "PERILH ! Aquesta aplicacion es encara experimentala (autrament dich, fonciona pas) e es possible que còpe lo sistèma ! Deuriatz PAS l’installar se non sabètz çò que fasètz. Volètz vertadièrament córrer aqueste risc ? [{answers}]",
+ "confirm_app_install_thirdparty": "ATENCION ! L’installacion d’aplicacions tèrças pòt comprometre l’integralitat e la seguretat del sistèma. Deuriatz PAS l’installar se non sabètz pas çò que fasètz. Volètz vertadièrament córrer aqueste risc ? [{answers}] ",
"dpkg_lock_not_available": "Aquesta comanda pòt pas s’executar pel moment perque un autre programa sembla utilizar lo varrolh de dpkg (lo gestionari de paquets del sistèma)",
"log_regen_conf": "Regenerar las configuracions del sistèma « {} »",
- "service_reloaded_or_restarted": "Lo servici « {service:s} » es estat recargat o reaviat",
+ "service_reloaded_or_restarted": "Lo servici « {service} » es estat recargat o reaviat",
"tools_upgrade_regular_packages_failed": "Actualizacion impossibla dels paquets seguents : {packages_list}",
"tools_upgrade_special_packages_completed": "L’actualizacion dels paquets de YunoHost es acabada !\nQuichatz [Entrada] per tornar a la linha de comanda",
"dpkg_is_broken": "Podètz pas far aquò pel moment perque dpkg/APT (los gestionaris de paquets del sistèma) sembla èsser mal configurat… Podètz ensajar de solucionar aquò en vos connectar via SSH e en executar « sudo dpkg --configure -a ».",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Autorizar l’utilizacion de la clau òst DSA (obsolèta) per la configuracion del servici SSH",
- "migration_0008_general_disclaimer": "Per melhorar la seguretat del servidor, es recomandat de daissar YunoHost gerir la configuracion SSH. Vòstra configuracion actuala es diferenta de la configuracion recomandada. Se daissatz YunoHost la reconfigurar, lo biais de vos connectar al servidor via SSH cambiarà coma aquò :",
- "hook_json_return_error": "Fracàs de la lectura del retorn de l’script {path:s}. Error : {msg:s}. Contengut brut : {raw_content}",
- "migration_0008_port": " - vos cal vos connectar en utilizar lo pòrt 22 allòc de vòstre pòrt SSH actual personalizat. Esitetz pas a lo reconfigurar ;",
- "migration_0009_not_needed": "Sembla qu’i aguèt ja una migracion. Passem.",
+ "hook_json_return_error": "Fracàs de la lectura del retorn de l’script {path}. Error : {msg}. Contengut brut : {raw_content}",
"pattern_password_app": "O planhèm, los senhals devon pas conténer los caractèrs seguents : {forbidden_chars}",
"regenconf_file_backed_up": "Lo fichièr de configuracion « {conf} » es estat salvagardat dins « {backup} »",
"regenconf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »",
@@ -409,16 +337,10 @@
"global_settings_setting_security_nginx_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor web NGINX Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)",
"global_settings_setting_security_ssh_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor SSH. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)",
"global_settings_setting_security_postfix_compatibility": "Solucion de compromés entre compatibilitat e seguretat pel servidor Postfix. Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)",
- "migration_description_0010_migrate_to_apps_json": "Levar las appslists despreciadas e utilizar la nòva lista unificada « apps.json » allòc",
- "migration_0008_root": " - vos poiretz pas vos connectar coma root via SSH. Allòc auretz d’utilizar l’utilizaire admin;",
- "migration_0008_warning": "Se comprenètz aquestes avertiments e qu’acceptatz de daissar YunoHost remplaçar la configuracion actuala, començatz la migracion. Autrament podètz tanben passar la migracion, encara que non siá pas recomandat.",
"service_regen_conf_is_deprecated": "« yunohost service regen-conf » es despreciat ! Utilizatz « yunohost tools regen-conf » allòc.",
- "service_reload_failed": "Impossible de recargar lo servici « {service:s} »\n\nJornal d’audit recent : {logs:s}",
- "service_restart_failed": "Impossible de reaviar lo servici « {service:s} »\n\nJornal d’audit recent : {logs:s}",
- "service_reload_or_restart_failed": "Impossible de recargar o reaviar lo servici « {service:s} »\n\nJornal d’audit recent : {logs:s}",
- "migration_description_0009_decouple_regenconf_from_services": "Desassociar lo mecanisme de regen-conf dels servicis",
- "migration_0008_dsa": " - la clau DSA serà desactivada. En consequéncia, poiriatz aver d’invalidar un messatge espaurugant del client SSH, e tornar verificar l’emprunta del servidor;",
- "migration_0008_no_warning": "Cap de risc important es estat detectat per remplaçar e la configuracion SSH, mas podèm pas n’èsser totalament segur ;) Se acceptatz que YunoHost remplace la configuracion actuala, començatz la migracion. Autrament, podètz passar la migracion, tot ben que non siá pas recomandat.",
+ "service_reload_failed": "Impossible de recargar lo servici « {service} »\n\nJornal d’audit recent : {logs}",
+ "service_restart_failed": "Impossible de reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}",
+ "service_reload_or_restart_failed": "Impossible de recargar o reaviar lo servici « {service} »\n\nJornal d’audit recent : {logs}",
"regenconf_file_kept_back": "S’espèra que lo fichièr de configuracion « {conf} » siá suprimit per regen-conf (categoria {category} mas es estat mantengut.",
"this_action_broke_dpkg": "Aquesta accion a copat dpkg/apt (los gestionaris de paquets del sistèma)… Podètz ensajar de resòlver aqueste problèma en vos connectant amb SSH e executant « sudo dpkg --configure -a ».",
"tools_upgrade_at_least_one": "Especificatz --apps O --system",
@@ -428,38 +350,25 @@
"tools_upgrade_special_packages_explanation": "Aquesta accion s’acabarà mas l’actualizacion especiala actuala contunharà en rèire-plan. Comencetz pas cap d’autra accion sul servidor dins las ~ 10 minutas que venon (depend de la velocitat de la maquina). Un còp acabat, benlèu que vos calrà vos tornar connectar a l’interfàcia d’administracion. Los jornals d’audit de l’actualizacion seràn disponibles a Aisinas > Jornals d’audit (dins l’interfàcia d’administracion) o amb « yunohost log list » (en linha de comanda).",
"update_apt_cache_failed": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}",
"update_apt_cache_warning": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}",
- "backup_permission": "Autorizacion de salvagarda per l’aplicacion {app:s}",
+ "backup_permission": "Autorizacion de salvagarda per l’aplicacion {app}",
"group_created": "Grop « {group} » creat",
"group_creation_failed": "Fracàs de la creacion del grop « {group} » : {error}",
"group_deleted": "Lo grop « {group} » es estat suprimit",
"group_deletion_failed": "Fracàs de la supression del grop « {group} » : {error}",
"group_unknown": "Lo grop « {group} » es desconegut",
"log_user_group_delete": "Suprimir lo grop « {} »",
- "migration_0011_backup_before_migration": "Creacion d’una còpia de seguretat de la basa de donadas LDAP e de la configuracion de las aplicacions abans d’efectuar la migracion.",
- "migration_0011_create_group": "Creacion d’un grop per cada utilizaire…",
- "migration_0011_done": "Migracion complèta. Ara podètz gerir de grops d’utilizaires.",
- "migration_0011_LDAP_update_failed": "Actualizacion impossibla de LDAP. Error : {error:s}",
- "migration_0011_migration_failed_trying_to_rollback": "La migracion a fracassat… ensag de tornar lo sistèma a l’estat anterio.",
- "migration_0011_rollback_success": "Restauracion del sistèma reüssida.",
"group_updated": "Lo grop « {group} » es estat actualizat",
"group_update_failed": "Actualizacion impossibla del grop « {group} » : {error}",
"log_user_group_update": "Actualizar lo grop « {} »",
- "migration_description_0011_setup_group_permission": "Configurar lo grop d’utilizaire e las permission de las aplicacions e dels servicis",
- "migration_0011_can_not_backup_before_migration": "La salvagarda del sistèma abans la migracion a pas capitat. La migracion a fracassat. Error : {error:s}",
- "migration_0011_migrate_permission": "Migracion de las permission dels paramètres d’aplicacion a LDAP…",
- "migration_0011_update_LDAP_database": "Actualizacion de la basa de donadas LDAP…",
- "migration_0011_update_LDAP_schema": "Actualizacion de l’esquèma LDAP…",
- "permission_already_exist": "La permission « {permission:s} » existís ja",
- "permission_created": "Permission « {permission:s} » creada",
+ "permission_already_exist": "La permission « {permission} » existís ja",
+ "permission_created": "Permission « {permission} » creada",
"permission_creation_failed": "Creacion impossibla de la permission",
- "permission_deleted": "Permission « {permission:s} » suprimida",
- "permission_deletion_failed": "Fracàs de la supression de la permission « {permission:s} »",
- "permission_not_found": "Permission « {permission:s} » pas trobada",
+ "permission_deleted": "Permission « {permission} » suprimida",
+ "permission_deletion_failed": "Fracàs de la supression de la permission « {permission} »",
+ "permission_not_found": "Permission « {permission} » pas trobada",
"permission_update_failed": "Fracàs de l’actualizacion de la permission",
- "permission_updated": "La permission « {permission:s} » es estada actualizada",
- "permission_update_nothing_to_do": "Cap de permission d’actualizar",
- "mailbox_disabled": "La bóstia de las letras es desactivada per l’utilizaire {user:s}",
- "migration_description_0012_postgresql_password_to_md5_authentication": "Forçar l’autentificacion PostgreSQL a utilizar MD5 per las connexions localas",
+ "permission_updated": "La permission « {permission} » es estada actualizada",
+ "mailbox_disabled": "La bóstia de las letras es desactivada per l’utilizaire {user}",
"migrations_success_forward": "Migracion {id} corrèctament realizada !",
"migrations_running_forward": "Execucion de la migracion {id}…",
"migrations_must_provide_explicit_targets": "Devètz fornir una cibla explicita quand utilizatz using --skip o --force-rerun",
@@ -471,10 +380,8 @@
"migrations_no_such_migration": "I a pas cap de migracion apelada « {id} »",
"migrations_not_pending_cant_skip": "Aquestas migracions son pas en espèra, las podètz pas doncas ignorar : {ids}",
"app_action_broke_system": "Aquesta accion sembla aver copat de servicis importants : {services}",
- "diagnosis_display_tip_web": "Podètz anar a la seccion Diagnostic (dins l’ecran d’acuèlh) per veire los problèmas trobats.",
"diagnosis_ip_no_ipv6": "Lo servidor a pas d’adreça IPv6 activa.",
"diagnosis_ip_not_connected_at_all": "Lo servidor sembla pas connectat a Internet ?!",
- "diagnosis_security_all_good": "Cap de vulnerabilitat de seguretat critica pas trobada.",
"diagnosis_description_regenconf": "Configuracion sistèma",
"diagnosis_http_ok": "Lo domeni {domain} accessible de l’exterior.",
"app_full_domain_unavailable": "Aquesta aplicacion a d’èsser installada sul seu pròpri domeni, mas i a d’autras aplicacions installadas sus aqueste domeni « {domain} ». Podètz utilizar allòc un josdomeni dedicat a aquesta aplicacion.",
@@ -487,7 +394,6 @@
"log_permission_url": "Actualizacion de l’URL ligada a la permission « {} »",
"app_install_failed": "Installacion impossibla de {app} : {error}",
"app_install_script_failed": "Una error s’es producha en installar lo script de l’aplicacion",
- "migration_0011_failed_to_remove_stale_object": "Supression impossibla d’un objècte obsolèt {dn} : {error}",
"apps_already_up_to_date": "Totas las aplicacions son ja al jorn",
"app_remove_after_failed_install": "Supression de l’aplicacion aprèp fracàs de l’installacion...",
"group_already_exist": "Lo grop {group} existís ja",
@@ -499,7 +405,6 @@
"diagnosis_basesystem_kernel": "Lo servidor fonciona amb lo nuclèu Linuxl {kernel_version}",
"diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})",
"diagnosis_basesystem_ynh_inconsistent_versions": "Utilizatz de versions inconsistentas dels paquets de YunoHost… probablament a causa d'una actualizacion fracassada o parciala.",
- "diagnosis_display_tip_cli": "Podètz executar « yunohost diagnosis show --issues » per mostrar las errors trobadas.",
"diagnosis_ignored_issues": "(+ {nb_ignored} problèma(es) ignorat(s))",
"diagnosis_everything_ok": "Tot sembla corrècte per {category} !",
"diagnosis_ip_connected_ipv4": "Lo servidor es connectat a Internet via IPv4 !",
@@ -522,13 +427,11 @@
"diagnosis_description_services": "Verificacion d’estat de servicis",
"diagnosis_description_systemresources": "Resorgas sistèma",
"diagnosis_description_ports": "Exposicion dels pòrts",
- "diagnosis_description_security": "Verificacion de seguretat",
"diagnosis_ports_unreachable": "Lo pòrt {port} es pas accessible de l’exterior.",
"diagnosis_ports_ok": "Lo pòrt {port} es accessible de l’exterior.",
"diagnosis_http_unreachable": "Lo domeni {domain} es pas accessible via HTTP de l’exterior.",
"diagnosis_unknown_categories": "La categorias seguentas son desconegudas : {categories}",
"diagnosis_ram_low": "Lo sistèma a {available} ({available_percent}%) de memòria RAM disponibla d’un total de {total}). Atencion.",
- "diagnosis_regenconf_manually_modified_debian": "Lo fichier de configuracion {file} foguèt modificat manualament respècte al fichièr per defaut de Debian.",
"log_permission_create": "Crear la permission « {} »",
"log_permission_delete": "Suprimir la permission « {} »",
"log_user_group_create": "Crear lo grop « {} »",
@@ -538,16 +441,14 @@
"diagnosis_found_warnings": "Trobat {warnings} element(s) que se poirián melhorar per {category}.",
"diagnosis_dns_missing_record": "Segon la configuracion DNS recomandada, vos calriá ajustar un enregistrament DNS\ntipe: {type}\nnom: {name}\nvalor: {value}",
"diagnosis_dns_discrepancy": "La configuracion DNS seguenta sembla pas la configuracion recomandada :
Tipe : {type}
Nom : {name}
Valors actualas : {current]
Valor esperada : {value}
",
- "diagnosis_regenconf_manually_modified_debian_details": "Es pas problematic, mas car téner d’agacher...",
"diagnosis_ports_could_not_diagnose": "Impossible de diagnosticar se los pòrts son accessibles de l’exterior.",
"diagnosis_ports_could_not_diagnose_details": "Error : {error}",
"diagnosis_http_could_not_diagnose": "Impossible de diagnosticar se lo domeni es accessible de l’exterior.",
"diagnosis_http_could_not_diagnose_details": "Error : {error}",
- "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion…",
+ "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion...",
"apps_catalog_failed_to_download": "Telecargament impossible del catalòg d’aplicacions {apps_catalog} : {error}",
"apps_catalog_obsolete_cache": "La memòria cache del catalòg d’aplicacion es voida o obsolèta.",
"apps_catalog_update_success": "Lo catalòg d’aplicacions es a jorn !",
- "diagnosis_mail_ougoing_port_25_ok": "Lo pòrt de sortida 25 es pas blocat e lo corrièr electronic pòt partir als autres servidors.",
"diagnosis_description_mail": "Corrièl",
"app_upgrade_script_failed": "Una error s’es producha pendent l’execucion de l’script de mesa a nivèl de l’aplicacion",
"diagnosis_cant_run_because_of_dep": "Execucion impossibla del diagnostic per {category} mentre que i a de problèmas importants ligats amb {dep}.",
@@ -560,13 +461,12 @@
"diagnosis_services_conf_broken": "La configuracion es copada pel servici {service} !",
"diagnosis_ports_needed_by": "Es necessari qu’aqueste pòrt siá accessible pel servici {service}",
"diagnosis_diskusage_low": "Lo lòc d’emmagazinatge {mountpoint}
(sul periferic {device}
) a solament {free} ({free_percent}%). Siatz prudent.",
- "migration_description_0014_remove_app_status_json": "Suprimir los fichièrs d’aplicacion status.json eretats",
"dyndns_provider_unreachable": "Impossible d’atenher lo provesidor Dyndns : siá vòstre YunoHost es pas corrèctament connectat a Internet siá lo servidor dynette es copat.",
"diagnosis_services_bad_status_tip": "Podètz ensajar de reaviar lo servici, e se non fonciona pas, podètz agachar los jornals de servici a la pagina web d’administracion(en linha de comanda podètz utilizar yunohost service restart {service} e yunohost service log {service} ).",
"diagnosis_http_connection_error": "Error de connexion : connexion impossibla al domeni demandat, benlèu qu’es pas accessible.",
"group_user_already_in_group": "L’utilizaire {user} es ja dins lo grop « {group} »",
- "diagnosis_ip_broken_resolvconf": "La resolucion del nom de domeni sembla copada sul servidor, poiriá èsser ligada al fait que /etc/resolv.conf manda pas a 127.0.0.1.",
- "diagnosis_ip_weird_resolvconf": "La resolucion del nom de domeni sembla foncionar, mas siatz prudent en utilizant un fichièr /etc/resolv.con personalizat.",
+ "diagnosis_ip_broken_resolvconf": "La resolucion del nom de domeni sembla copada sul servidor, poiriá èsser ligada al fait que /etc/resolv.conf
manda pas a 127.0.0.1
.",
+ "diagnosis_ip_weird_resolvconf": "La resolucion del nom de domeni sembla foncionar, mas sembla qu’utiilizatz un fichièr /etc/resolv.conf
personalizat.",
"diagnosis_diskusage_verylow": "Lo lòc d’emmagazinatge {mountpoint}
(sul periferic {device}
) a solament {free} ({free_percent}%). Deuriatz considerar de liberar un pauc d’espaci.",
"global_settings_setting_pop3_enabled": "Activar lo protocòl POP3 pel servidor de corrièr",
"diagnosis_diskusage_ok": "Lo lòc d’emmagazinatge {mountpoint}
(sul periferic {device}
) a encara {free} ({free_percent}%) de liure !",
@@ -578,7 +478,6 @@
"diagnosis_mail_ehlo_could_not_diagnose_details": "Error : {error}",
"diagnosis_mail_queue_unavailable_details": "Error : {error}",
"diagnosis_basesystem_hardware": "L’arquitectura del servidor es {virt} {arch}",
- "diagnosis_basesystem_hardware_board": "Lo modèl de carta del servidor es {model}",
"backup_archive_corrupted": "Sembla que l’archiu de la salvagarda « {archive} » es corromput : {error}",
"diagnosis_domain_expires_in": "{domain} expiraà d’aquí {days} jorns.",
"migration_0015_cleaning_up": "Netejatge de la memòria cache e dels paquets pas mai necessaris…",
@@ -592,6 +491,26 @@
"app_manifest_install_ask_domain": "Causissètz lo domeni ont volètz installar aquesta aplicacion",
"app_argument_password_no_default": "Error pendent l’analisi de l’argument del senhal « {name} » : l’argument de senhal pòt pas aver de valor per defaut per de rason de seguretat",
"app_label_deprecated": "Aquesta comanda es estada renduda obsolèta. Mercés d'utilizar lo nòva \"yunohost user permission update\" per gerir letiquetada de l'aplication",
- "additional_urls_already_removed": "URL addicionala {url:s} es ja estada elimida per la permission «#permission:s»",
- "additional_urls_already_added": "URL addicionadal «{url:s}'» es ja estada aponduda per la permission «{permission:s}»"
-}
+ "additional_urls_already_removed": "URL addicionala {url} es ja estada elimida per la permission «#permission:s»",
+ "additional_urls_already_added": "URL addicionadal «{url}'» es ja estada aponduda per la permission «{permission}»",
+ "migration_0015_yunohost_upgrade": "Aviada de la mesa a jorn de YunoHost...",
+ "migration_0015_main_upgrade": "Aviada de la mesa a nivèl generala...",
+ "migration_0015_patching_sources_list": "Mesa a jorn del fichièr sources.lists...",
+ "migration_0015_start": "Aviar la migracion cap a Buster",
+ "migration_description_0017_postgresql_9p6_to_11": "Migrar las basas de donadas de PostgreSQL 9.6 cap a 11",
+ "migration_description_0016_php70_to_php73_pools": "Migrar los fichièrs de configuracion php7.0 cap a php7.3",
+ "migration_description_0015_migrate_to_buster": "Mesa a nivèl dels sistèmas Debian Buster e YunoHost 4.x",
+ "migrating_legacy_permission_settings": "Migracion dels paramètres de permission ancians...",
+ "log_app_action_run": "Executar l’accion de l’aplicacion « {} »",
+ "diagnosis_basesystem_hardware_model": "Lo modèl del servidor es {model}",
+ "backup_archive_cant_retrieve_info_json": "Obtencion impossibla de las informacions de l’archiu « {archive} »... Se pòt pas recuperar lo fichièr info.json (o es pas un fichièr json valid).",
+ "app_packaging_format_not_supported": "Se pòt pas installar aquesta aplicacion pr’amor que son format es pas pres en carga per vòstra version de YunoHost. Deuriatz considerar actualizar lo sistèma.",
+ "diagnosis_mail_fcrdns_ok": "Vòstre DNS inverse es corrèctament configurat !",
+ "diagnosis_mail_outgoing_port_25_ok": "Lo servidor de messatge SMTP pòt enviar de corrièls (lo pòrt 25 es pas blocat).",
+ "diagnosis_domain_expiration_warning": "D’unes domenis expiraràn lèu !",
+ "diagnosis_domain_expiration_success": "Vòstres domenis son enregistrats e expiraràn pas lèu.",
+ "diagnosis_domain_not_found_details": "Lo domeni {domain} existís pas a la basa de donadas WHOIS o a expirat !",
+ "diagnosis_domain_expiration_not_found": "Impossible de verificar la data d’expiracion d’unes domenis",
+ "backup_create_size_estimation": "L’archiu contendrà apr’aquí {size} de donadas.",
+ "app_restore_script_failed": "Una error s’es producha a l’interior del script de restauracion de l’aplicacion"
+}
\ No newline at end of file
diff --git a/locales/pl.json b/locales/pl.json
index 7ff9fbcd2..caf108367 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -1,3 +1,12 @@
{
- "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków"
+ "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków",
+ "app_already_up_to_date": "{app} jest obecnie aktualna",
+ "app_already_installed": "{app} jest już zainstalowane",
+ "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.",
+ "admin_password_too_long": "Proszę wybrać hasło krótsze niż 127 znaków",
+ "admin_password_changed": "Hasło administratora zostało zmienione",
+ "admin_password_change_failed": "Nie można zmienić hasła",
+ "admin_password": "Hasło administratora",
+ "action_invalid": "Nieprawidłowa operacja '{action}'",
+ "aborting": "Przerywanie."
}
\ No newline at end of file
diff --git a/locales/pt.json b/locales/pt.json
index 9375f2354..d285948be 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -1,64 +1,54 @@
{
- "action_invalid": "Acção Inválida '{action:s}'",
+ "action_invalid": "Acção Inválida '{action}'",
"admin_password": "Senha de administração",
- "admin_password_change_failed": "Não é possível alterar a senha",
+ "admin_password_change_failed": "Não foi possível alterar a senha",
"admin_password_changed": "A senha da administração foi alterada",
- "app_already_installed": "{app:s} já está instalada",
- "app_extraction_failed": "Não foi possível extrair os ficheiros para instalação",
- "app_id_invalid": "A ID da aplicação é inválida",
- "app_install_files_invalid": "Ficheiros para instalação corrompidos",
+ "app_already_installed": "{app} já está instalada",
+ "app_extraction_failed": "Não foi possível extrair os arquivos para instalação",
+ "app_id_invalid": "App ID invaĺido",
+ "app_install_files_invalid": "Esses arquivos não podem ser instalados",
"app_manifest_invalid": "Manifesto da aplicação inválido: {error}",
- "app_not_installed": "{app:s} não está instalada",
- "app_removed": "{app:s} removida com êxito",
- "app_sources_fetch_failed": "Incapaz obter os ficheiros fonte",
+ "app_not_installed": "Não foi possível encontrar {app} na lista de aplicações instaladas: {all_apps}",
+ "app_removed": "{app} desinstalada",
+ "app_sources_fetch_failed": "Não foi possível carregar os arquivos de código fonte, a URL está correta?",
"app_unknown": "Aplicação desconhecida",
- "app_upgrade_failed": "Não foi possível atualizar {app:s}",
- "app_upgraded": "{app:s} atualizada com sucesso",
- "ask_email": "Endereço de Email",
+ "app_upgrade_failed": "Não foi possível atualizar {app}: {error}",
+ "app_upgraded": "{app} atualizado",
"ask_firstname": "Primeiro nome",
"ask_lastname": "Último nome",
"ask_main_domain": "Domínio principal",
"ask_new_admin_password": "Nova senha de administração",
"ask_password": "Senha",
"backup_created": "Backup completo",
- "backup_invalid_archive": "Arquivo de backup inválido",
- "backup_output_directory_not_empty": "A pasta de destino não se encontra vazia",
- "custom_app_url_required": "Deve fornecer um link para atualizar a sua aplicação personalizada {app:s}",
+ "backup_output_directory_not_empty": "Você deve escolher um diretório de saída que esteja vazio",
+ "custom_app_url_required": "Deve fornecer um link para atualizar a sua aplicação personalizada {app}",
"domain_cert_gen_failed": "Não foi possível gerar o certificado",
"domain_created": "Domínio criado com êxito",
- "domain_creation_failed": "Não foi possível criar o domínio",
+ "domain_creation_failed": "Não foi possível criar o domínio {domain}: {error}",
"domain_deleted": "Domínio removido com êxito",
- "domain_deletion_failed": "Não foi possível eliminar o domínio",
+ "domain_deletion_failed": "Não foi possível eliminar o domínio {domain}: {error}",
"domain_dyndns_already_subscribed": "Já subscreveu um domínio DynDNS",
"domain_dyndns_root_unknown": "Domínio root (administrador) DynDNS desconhecido",
"domain_exists": "O domínio já existe",
"domain_uninstall_app_first": "Existem uma ou mais aplicações instaladas neste domínio. Por favor desinstale-as antes de proceder com a remoção do domínio.",
- "domain_unknown": "Domínio desconhecido",
"done": "Concluído.",
"downloading": "Transferência em curso...",
- "dyndns_cron_installed": "Gestor de tarefas cron DynDNS instalado com êxito",
- "dyndns_cron_remove_failed": "Não foi possível remover o gestor de tarefas cron DynDNS",
- "dyndns_cron_removed": "Gestor de tarefas cron DynDNS removido com êxito",
- "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP a partir de DynDNS",
- "dyndns_ip_updated": "Endereço IP atualizado com êxito a partir de DynDNS",
+ "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP para DynDNS",
+ "dyndns_ip_updated": "Endereço IP atualizado com êxito para DynDNS",
"dyndns_key_generating": "A chave DNS está a ser gerada, isto pode demorar um pouco...",
"dyndns_registered": "Dom+inio DynDNS registado com êxito",
- "dyndns_registration_failed": "Não foi possível registar o domínio DynDNS: {error:s}",
- "dyndns_unavailable": "Subdomínio DynDNS indisponível",
- "executing_script": "A executar o script...",
+ "dyndns_registration_failed": "Não foi possível registar o domínio DynDNS: {error}",
+ "dyndns_unavailable": "O domínio '{domain}' não está disponível.",
"extracting": "Extração em curso...",
- "field_invalid": "Campo inválido '{:s}'",
+ "field_invalid": "Campo inválido '{}'",
"firewall_reloaded": "Firewall recarregada com êxito",
"installation_complete": "Instalação concluída",
- "installation_failed": "A instalação falhou",
"iptables_unavailable": "Não pode alterar aqui a iptables. Ou o seu kernel não o suporta ou está num espaço reservado.",
- "ldap_initialized": "LDAP inicializada com êxito",
- "mail_alias_remove_failed": "Não foi possível remover a etiqueta de correio '{mail:s}'",
- "mail_domain_unknown": "Domínio de endereço de correio '{domain:s}' inválido. Por favor, usa um domínio administrado per esse servidor.",
- "mail_forward_remove_failed": "Não foi possível remover o reencaminhamento de correio '{mail:s}'",
+ "mail_alias_remove_failed": "Não foi possível remover a etiqueta de correio '{mail}'",
+ "mail_domain_unknown": "Domínio de endereço de correio '{domain}' inválido. Por favor, usa um domínio administrado per esse servidor.",
+ "mail_forward_remove_failed": "Não foi possível remover o reencaminhamento de correio '{mail}'",
"main_domain_change_failed": "Incapaz alterar o domínio raiz",
"main_domain_changed": "Domínio raiz alterado com êxito",
- "no_internet_connection": "O servidor não está ligado à Internet",
"packages_upgrade_failed": "Não foi possível atualizar todos os pacotes",
"pattern_domain": "Deve ser um nome de domínio válido (p.e. meu-dominio.org)",
"pattern_email": "Deve ser um endereço de correio válido (p.e. alguem@dominio.org)",
@@ -66,23 +56,23 @@
"pattern_lastname": "Deve ser um último nome válido",
"pattern_password": "Deve ter no mínimo 3 caracteres",
"pattern_username": "Devem apenas ser carácteres minúsculos alfanuméricos e subtraços",
- "restore_confirm_yunohost_installed": "Quer mesmo restaurar um sistema já instalado? [{answers:s}]",
- "service_add_failed": "Incapaz adicionar serviço '{service:s}'",
+ "restore_confirm_yunohost_installed": "Quer mesmo restaurar um sistema já instalado? [{answers}]",
+ "service_add_failed": "Incapaz adicionar serviço '{service}'",
"service_added": "Serviço adicionado com êxito",
- "service_already_started": "O serviço '{service:s}' já está em execussão",
- "service_already_stopped": "O serviço '{service:s}' já está parado",
- "service_cmd_exec_failed": "Incapaz executar o comando '{command:s}'",
- "service_disable_failed": "Incapaz desativar o serviço '{service:s}'",
- "service_disabled": "O serviço '{service:s}' foi desativado com êxito",
- "service_enable_failed": "Incapaz de ativar o serviço '{service:s}'",
- "service_enabled": "Serviço '{service:s}' ativado com êxito",
- "service_remove_failed": "Incapaz de remover o serviço '{service:s}'",
+ "service_already_started": "O serviço '{service}' já está em execussão",
+ "service_already_stopped": "O serviço '{service}' já está parado",
+ "service_cmd_exec_failed": "Incapaz executar o comando '{command}'",
+ "service_disable_failed": "Incapaz desativar o serviço '{service}'",
+ "service_disabled": "O serviço '{service}' foi desativado com êxito",
+ "service_enable_failed": "Incapaz de ativar o serviço '{service}'",
+ "service_enabled": "Serviço '{service}' ativado com êxito",
+ "service_remove_failed": "Incapaz de remover o serviço '{service}'",
"service_removed": "Serviço eliminado com êxito",
- "service_start_failed": "Não foi possível iniciar o serviço '{service:s}'",
- "service_started": "O serviço '{service:s}' foi iniciado com êxito",
- "service_stop_failed": "Incapaz parar o serviço '{service:s}'",
- "service_stopped": "O serviço '{service:s}' foi parado com êxito",
- "service_unknown": "Serviço desconhecido '{service:s}'",
+ "service_start_failed": "Não foi possível iniciar o serviço '{service}'",
+ "service_started": "O serviço '{service}' foi iniciado com êxito",
+ "service_stop_failed": "Incapaz parar o serviço '{service}'",
+ "service_stopped": "O serviço '{service}' foi parado com êxito",
+ "service_unknown": "Serviço desconhecido '{service}'",
"ssowat_conf_generated": "Configuração SSOwat gerada com êxito",
"ssowat_conf_updated": "Configuração persistente SSOwat atualizada com êxito",
"system_upgraded": "Sistema atualizado com êxito",
@@ -99,46 +89,170 @@
"user_update_failed": "Não foi possível atualizar o utilizador",
"user_updated": "Utilizador atualizado com êxito",
"yunohost_already_installed": "AYunoHost já está instalado",
- "yunohost_ca_creation_failed": "Incapaz criar o certificado de autoridade",
"yunohost_configured": "YunoHost configurada com êxito",
"yunohost_installing": "A instalar a YunoHost...",
"yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'.",
- "app_not_correctly_installed": "{app:s} parece não estar corretamente instalada",
- "app_not_properly_removed": "{app:s} não foi corretamente removido",
+ "app_not_correctly_installed": "{app} parece não estar corretamente instalada",
+ "app_not_properly_removed": "{app} não foi corretamente removido",
"app_requirements_checking": "Verificando os pacotes necessários para {app}...",
"app_unsupported_remote_type": "A aplicação não possui suporte ao tipo remoto utilizado",
- "backup_archive_app_not_found": "A aplicação '{app:s}' não foi encontrada no arquivo de backup",
- "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado ao {path:s})",
- "backup_archive_name_exists": "O nome do arquivo de backup já existe",
- "backup_archive_open_failed": "Não é possível abrir o arquivo de backup",
- "backup_cleaning_failed": "Não é possível limpar a pasta temporária de backups",
- "backup_creation_failed": "A criação do backup falhou",
- "backup_delete_error": "Impossível apagar '{path:s}'",
- "backup_deleted": "O backup foi suprimido",
- "backup_hook_unknown": "Gancho de backup '{hook:s}' desconhecido",
- "backup_nothings_done": "Não há nada para guardar",
- "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas",
- "app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Olhe para o `app changeurl` se estiver disponível.",
- "app_already_up_to_date": "{app:s} já está atualizado",
- "app_argument_choice_invalid": "Escolha inválida para o argumento '{name:s}', deve ser um dos {choices:s}",
- "app_argument_invalid": "Valor inválido de argumento '{name:s}': {error:s}",
- "app_argument_required": "O argumento '{name:s}' é obrigatório",
- "app_change_url_failed_nginx_reload": "Falha ao reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors:s}",
- "app_location_unavailable": "Esta url não está disponível ou está em conflito com outra aplicação já instalada",
- "app_upgrade_app_name": "Atualizando aplicação {app}…",
+ "backup_archive_app_not_found": "Não foi possível encontrar {app} no arquivo de backup",
+ "backup_archive_broken_link": "Não foi possível acessar o arquivo de backup (link quebrado ao {path})",
+ "backup_archive_name_exists": "Já existe um arquivo de backup com esse nome.",
+ "backup_archive_open_failed": "Não foi possível abrir o arquivo de backup",
+ "backup_cleaning_failed": "Não foi possível limpar o diretório temporário de backup",
+ "backup_creation_failed": "Não foi possível criar o arquivo de backup",
+ "backup_delete_error": "Não foi possível remover '{path}'",
+ "backup_deleted": "Backup removido",
+ "backup_hook_unknown": "O gancho de backup '{hook}' é desconhecido",
+ "backup_nothings_done": "Nada há se salvar",
+ "backup_output_directory_forbidden": "Escolha um diretório de saída diferente. Backups não podem ser criados nos subdiretórios /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
+ "app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Confira em `app changeurl` se está disponível.",
+ "app_already_up_to_date": "{app} já está atualizado",
+ "app_argument_choice_invalid": "Escolha um valor válido para o argumento '{name}' : '{value}' não está entre as opções disponíveis ({choices})",
+ "app_argument_invalid": "Escolha um valor válido para o argumento '{name}': {error}",
+ "app_argument_required": "O argumento '{name}' é obrigatório",
+ "app_location_unavailable": "Esta url ou não está disponível ou está em conflito com outra(s) aplicação(ões) já instalada(s):\n{apps}",
+ "app_upgrade_app_name": "Atualizando {app}…",
"app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações",
- "backup_abstract_method": "Este metodo de backup ainda não foi implementado",
- "backup_app_failed": "Não foi possível fazer o backup dos aplicativos '{app:s}'",
- "backup_applying_method_custom": "Chamando o metodo personalizado de backup '{method:s}'…",
- "backup_applying_method_tar": "Criando o arquivo tar de backup…",
- "backup_archive_name_unknown": "Desconhece-se o arquivo local de backup de nome '{name:s}'",
- "backup_archive_system_part_not_available": "A seção do sistema '{part:s}' está indisponivel neste backup",
- "backup_ask_for_copying_if_needed": "Alguns arquivos não consiguiram ser preparados para backup utilizando o metodo que não gasta espaço de disco temporariamente. Para realizar o backup {size:s}MB precisam ser usados temporariamente. Você concorda?",
- "backup_borg_not_implemented": "O método de backup Borg ainda não foi implementado.",
- "backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido",
- "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo",
- "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain:s}{path:s}'), nada para fazer.",
+ "backup_abstract_method": "Este método de backup ainda não foi implementado",
+ "backup_app_failed": "Não foi possível fazer o backup de '{app}'",
+ "backup_applying_method_custom": "Chamando o método personalizado de backup '{method}'…",
+ "backup_applying_method_tar": "Criando o arquivo TAR de backup…",
+ "backup_archive_name_unknown": "Desconhece-se o arquivo local de backup de nome '{name}'",
+ "backup_archive_system_part_not_available": "A seção do sistema '{part}' está indisponível neste backup",
+ "backup_ask_for_copying_if_needed": "Você quer efetuar o backup usando {size}MB temporariamente? (E necessário fazer dessa forma porque alguns arquivos não puderam ser preparados usando um método mais eficiente)",
+ "backup_cant_mount_uncompress_archive": "Não foi possível montar o arquivo descomprimido como protegido contra escrita",
+ "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo",
+ "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain}{path}'), nada para fazer.",
"password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres",
"admin_password_too_long": "Escolha uma senha que contenha menos de 127 caracteres",
- "aborting": "Abortando."
+ "aborting": "Abortando.",
+ "app_change_url_no_script": "A aplicação '{app_name}' ainda não permite modificar a URL. Talvez devesse atualizá-la.",
+ "app_argument_password_no_default": "Erro ao interpretar argumento da senha '{name}': O argumento da senha não pode ter um valor padrão por segurança",
+ "app_action_cannot_be_ran_because_required_services_down": "Estes serviços devem estar funcionado para executar esta ação: {services}. Tente reiniciá-los para continuar (e possivelmente investigar o porquê de não estarem funcionado).",
+ "app_action_broke_system": "Esta ação parece ter quebrado estes serviços importantes: {services}",
+ "already_up_to_date": "Nada a ser feito. Tudo já está atualizado.",
+ "additional_urls_already_removed": "A URL adicional '{url}'já está removida para a permissão '{permission}'",
+ "additional_urls_already_added": "A URL adicional '{url}' já está adicionada para a permissão '{permission}'",
+ "app_install_script_failed": "Ocorreu um erro dentro do script de instalação do aplicativo",
+ "app_install_failed": "Não foi possível instalar {app}: {error}",
+ "app_full_domain_unavailable": "Desculpe, esse app deve ser instalado num domínio próprio mas já há outros apps instalados no domínio '{domain}'. Você pode usar um subdomínio dedicado a esse aplicativo.",
+ "app_change_url_success": "A URL agora é {domain}{path}",
+ "apps_catalog_obsolete_cache": "O cache do catálogo de aplicações está vazio ou obsoleto.",
+ "apps_catalog_failed_to_download": "Não foi possível fazer o download do catálogo de aplicações {apps_catalog}: {error}",
+ "apps_catalog_updating": "Atualizando o catálogo de aplicações...",
+ "apps_catalog_init_success": "Catálogo de aplicações do sistema inicializado!",
+ "apps_already_up_to_date": "Todas as aplicações já estão atualizadas",
+ "app_packaging_format_not_supported": "Essa aplicação não pode ser instalada porque o formato dela não é suportado pela sua versão do YunoHost. Considere atualizar seu sistema.",
+ "app_upgrade_script_failed": "Ocorreu um erro dentro do script de atualização da aplicação",
+ "app_upgrade_several_apps": "As seguintes aplicações serão atualizadas: {apps}",
+ "app_start_restore": "Restaurando {app}...",
+ "app_start_backup": "Obtendo os arquivos para fazer o backup de {app}...",
+ "app_start_remove": "Removendo {app}...",
+ "app_start_install": "Instalando {app}...",
+ "app_restore_script_failed": "Ocorreu um erro dentro do script de restauração da aplicação",
+ "app_restore_failed": "Não foi possível restaurar {app}: {error}",
+ "app_remove_after_failed_install": "Removendo a aplicação após a falha da instalação...",
+ "app_requirements_unmeet": "Os requisitos para a aplicação {app} não foram satisfeitos, o pacote {pkgname} ({version}) devem ser {spec}",
+ "app_not_upgraded": "Não foi possível atualizar a aplicação '{failed_app}' e, como consequência, a atualização das seguintes aplicações foi cancelada: {apps}",
+ "app_manifest_install_ask_is_public": "Essa aplicação deve ser visível para visitantes anônimos?",
+ "app_manifest_install_ask_admin": "Escolha um usuário de administrador para essa aplicação",
+ "app_manifest_install_ask_password": "Escolha uma senha de administrador para essa aplicação",
+ "app_manifest_install_ask_path": "Escolha o caminho da url (depois do domínio) em que essa aplicação deve ser instalada",
+ "app_manifest_install_ask_domain": "Escolha o domínio em que esta aplicação deve ser instalada",
+ "app_label_deprecated": "Este comando está deprecado! Por favor use o novo comando 'yunohost user permission update' para gerenciar a etiqueta da aplicação.",
+ "app_make_default_location_already_used": "Não foi passível fazer a aplicação '{app}' ser a padrão no domínio, '{domain}' já está sendo usado por '{other_app}'",
+ "backup_archive_writing_error": "Não foi possível adicionar os arquivos '{source}' (nomeados dentro do arquivo '{dest}') ao backup no arquivo comprimido '{archive}'",
+ "backup_archive_corrupted": "Parece que o arquivo de backup '{archive}' está corrompido: {error}",
+ "backup_archive_cant_retrieve_info_json": "Não foi possível carregar informações para o arquivo '{archive}'... Não foi possível carregar info.json (ou não é um JSON válido).",
+ "backup_applying_method_copy": "Copiando todos os arquivos para o backup...",
+ "backup_actually_backuping": "Criando cópia de backup dos arquivos obtidos...",
+ "ask_user_domain": "Domínio para usar para o endereço de email e conta XMPP do usuário",
+ "ask_new_path": "Novo caminho",
+ "ask_new_domain": "Novo domínio",
+ "apps_catalog_update_success": "O catálogo de aplicações foi atualizado!",
+ "backup_no_uncompress_archive_dir": "Não existe tal diretório de arquivo descomprimido",
+ "backup_mount_archive_for_restore": "Preparando o arquivo para restauração...",
+ "backup_method_tar_finished": "Arquivo de backup TAR criado",
+ "backup_method_custom_finished": "Método de backup personalizado '{method}' finalizado",
+ "backup_method_copy_finished": "Cópia de backup finalizada",
+ "backup_custom_mount_error": "O método personalizado de backup não pôde passar do passo de 'mount'",
+ "backup_custom_backup_error": "O método personalizado de backup não pôde passar do passo de 'backup'",
+ "backup_csv_creation_failed": "Não foi possível criar o arquivo CSV necessário para a restauração",
+ "backup_csv_addition_failed": "Não foi possível adicionar os arquivos que estarão no backup ao arquivo CSV",
+ "backup_create_size_estimation": "O arquivo irá conter cerca de {size} de dados.",
+ "backup_couldnt_bind": "Não foi possível vincular {src} ao {dest}.",
+ "certmanager_attempt_to_replace_valid_cert": "Você está tentando sobrescrever um certificado bom e válido para o domínio {domain}! (Use --force para prosseguir mesmo assim)",
+ "backup_with_no_restore_script_for_app": "A aplicação {app} não tem um script de restauração, você não será capaz de automaticamente restaurar o backup dessa aplicação.",
+ "backup_with_no_backup_script_for_app": "A aplicação '{app}' não tem um script de backup. Ignorando.",
+ "backup_unable_to_organize_files": "Não foi possível usar o método rápido de organizar os arquivos no arquivo de backup",
+ "backup_system_part_failed": "Não foi possível fazer o backup da parte do sistema '{part}'",
+ "backup_running_hooks": "Executando os hooks de backup...",
+ "backup_permission": "Permissão de backup para {app}",
+ "backup_output_symlink_dir_broken": "O diretório de seu arquivo '{path}' é um link simbólico quebrado. Talvez você tenha esquecido de re/montar ou conectar o dispositivo de armazenamento para onde o link aponta.",
+ "backup_output_directory_required": "Você deve especificar um diretório de saída para o backup",
+ "diagnosis_description_apps": "Aplicações",
+ "diagnosis_apps_allgood": "Todos os apps instalados respeitam práticas básicas de empacotamento",
+ "diagnosis_apps_issue": "Um problema foi encontrado para o app {app}",
+ "diagnosis_apps_not_in_app_catalog": "Esta aplicação não está no catálogo de aplicações do YunoHost. Se estava no passado e foi removida, você deve considerar desinstalar este app já que ele não mais receberá atualizações e pode comprometer a integridade e segurança do seu sistema.",
+ "diagnosis_apps_broken": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.",
+ "diagnosis_apps_bad_quality": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.",
+ "diagnosis_apps_outdated_ynh_requirement": "A versão instalada deste app requer tão somente yunohost >= 2.x, o que tende a indicar que o app não está atualizado com as práticas de empacotamento recomendadas. Você deve considerar seriamente atualizá-lo.",
+ "diagnosis_apps_deprecated_practices": "A versão instalada deste app usa práticas de empacotamento extremamente velhas que não são mais usadas. Você deve considerar seriamente atualizá-lo.",
+ "certmanager_domain_http_not_working": "O domínio {domain} não parece estar acessível por HTTP. Por favor cheque a categoria 'Web' no diagnóstico para mais informações. (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)",
+ "diagnosis_description_regenconf": "Configurações do sistema",
+ "diagnosis_description_services": "Cheque de status dos serviços",
+ "diagnosis_basesystem_hardware": "A arquitetura hardware do servidor é {virt} {arch}",
+ "diagnosis_description_web": "Web",
+ "diagnosis_basesystem_ynh_single_version": "Versão {package}: {version} ({repo})",
+ "diagnosis_basesystem_ynh_main_version": "O servidor está rodando YunoHost {main_version} ({repo})",
+ "app_config_unable_to_apply": "Falha ao aplicar valores do painel de configuração.",
+ "app_config_unable_to_read": "Falha ao ler valores do painel de configuração.",
+ "config_apply_failed": "Aplicar as novas configuração falhou: {error}",
+ "config_cant_set_value_on_section": "Você não pode setar um único valor na seção de configuração inteira.",
+ "config_validate_time": "Deve ser um horário válido como HH:MM",
+ "config_validate_url": "Deve ser uma URL válida",
+ "config_version_not_supported": "Versões do painel de configuração '{version}' não são suportadas.",
+ "danger": "Perigo:",
+ "diagnosis_basesystem_ynh_inconsistent_versions": "Você está executando versões inconsistentes dos pacotes YunoHost... provavelmente por causa de uma atualização parcial ou que falhou.",
+ "diagnosis_description_basesystem": "Sistema base",
+ "certmanager_cert_signing_failed": "Não foi possível assinar o novo certificado",
+ "certmanager_unable_to_parse_self_CA_name": "Não foi possível processar nome da autoridade de auto-assinatura (arquivo: {file})",
+ "confirm_app_install_warning": "Aviso: Pode ser que essa aplicação funcione, mas ela não está bem integrada ao YunoHost. Algumas funcionalidades como single sign-on e backup/restauração podem não estar disponíveis. Instalar mesmo assim? [{answers}] ",
+ "config_forbidden_keyword": "A palavra chave '{keyword}' é reservada, você não pode criar ou usar um painel de configuração com uma pergunta com esse id.",
+ "config_no_panel": "Painel de configuração não encontrado.",
+ "config_unknown_filter_key": "A chave de filtro '{filter_key}' está incorreta.",
+ "config_validate_color": "Deve ser uma cor RGB hexadecimal válida",
+ "config_validate_date": "Deve ser uma data válida como no formato AAAA-MM-DD",
+ "config_validate_email": "Deve ser um email válido",
+ "diagnosis_basesystem_kernel": "O servidor está rodando Linux kernel {kernel_version}",
+ "diagnosis_cache_still_valid": "(O cache para a categoria de diagnóstico {category} ainda é valido. Não será diagnosticada novamente ainda)",
+ "diagnosis_cant_run_because_of_dep": "Impossível fazer diagnóstico para {category} enquanto ainda existem problemas importantes relacionados a {dep}.",
+ "diagnosis_diskusage_low": "Unidade de armazenamento {mountpoint}
(no dispositivo {device}
_) tem somente {free} ({free_percent}%) de espaço restante (de {total}). Tenha cuidado.",
+ "diagnosis_description_ip": "Conectividade internet",
+ "diagnosis_description_dnsrecords": "Registros DNS",
+ "diagnosis_description_mail": "Email",
+ "certmanager_domain_not_diagnosed_yet": "Ainda não há resultado de diagnóstico para o domínio {domain}. Por favor re-execute um diagnóstico para as categorias 'Registros DNS' e 'Web' na seção de diagnósticos para checar se o domínio está pronto para o Let's Encrypt. (Ou, se você souber o que está fazendo, use '--no-checks' para desativar estas checagens.)",
+ "diagnosis_basesystem_host": "O Servidor está rodando Debian {debian_version}",
+ "diagnosis_description_systemresources": "Recursos do sistema",
+ "certmanager_acme_not_configured_for_domain": "O challenge ACME não pode ser realizado para {domain} porque o código correspondente na configuração do nginx está ausente... Por favor tenha certeza de que sua configuração do nginx está atualizada executando o comando `yunohost tools regen-conf nginx --dry-run --with-diff`.",
+ "certmanager_attempt_to_renew_nonLE_cert": "O certificado para o domínio '{domain}' não foi emitido pelo Let's Encrypt. Não é possível renová-lo automaticamente!",
+ "certmanager_attempt_to_renew_valid_cert": "O certificado para o domínio '{domain}' não esta prestes a expirar! (Você pode usar --force se saber o que está fazendo)",
+ "certmanager_cannot_read_cert": "Algo de errado aconteceu ao tentar abrir o atual certificado para o domínio {domain} (arquivo: {file}), motivo: {reason}",
+ "certmanager_cert_install_success": "Certificado Let's Encrypt foi instalado para o domínio '{domain}'",
+ "certmanager_cert_install_success_selfsigned": "Certificado autoassinado foi instalado para o domínio '{domain}'",
+ "certmanager_certificate_fetching_or_enabling_failed": "Tentativa de usar o novo certificado para o domínio {domain} não funcionou...",
+ "certmanager_domain_cert_not_selfsigned": "O certificado para o domínio {domain} não é autoassinado. Você tem certeza que quer substituí-lo? (Use '--force' para fazê-lo)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "O registro de DNS para o domínio '{domain}' é diferente do IP deste servidor. Por favor cheque a categoria 'Registros DNS' (básico) no diagnóstico para mais informações. Se você modificou recentemente o registro 'A', espere um tempo para ele se propagar (alguns serviços de checagem de propagação de DNS estão disponíveis online). (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)",
+ "certmanager_hit_rate_limit": "Foram emitidos certificados demais para este conjunto de domínios {domain} recentemente. Por favor tente novamente mais tarde. Veja https://letsencrypt.org/docs/rate-limits/ para mais detalhes",
+ "certmanager_no_cert_file": "Não foi possível ler o arquivo de certificado para o domínio {domain} (arquivo: {file})",
+ "certmanager_self_ca_conf_file_not_found": "Não foi possível encontrar o arquivo de configuração para a autoridade de auto-assinatura (arquivo: {file})",
+ "confirm_app_install_danger": "ATENÇÃO! Sabe-se que esta aplicação ainda é experimental (isso se não que explicitamente não funciona)! Você provavelmente NÃO deve instalar ela a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se esta aplicação não funcionar ou quebrar o seu sistema... Se você está disposto a tomar esse rico de toda forma, digite '{answers}'",
+ "confirm_app_install_thirdparty": "ATENÇÃO! Essa aplicação não faz parte do catálogo do YunoHost. Instalar aplicações de terceiros pode comprometer a integridade e segurança do seu sistema. Você provavelmente NÃO deve instalá-la a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se este app não funcionar ou quebrar seu sistema... Se você está disposto a tomar este risco de toda forma, digite '{answers}'",
+ "diagnosis_description_ports": "Exposição de portas",
+ "diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}",
+ "diagnosis_backports_in_sources_list": "Parece que o apt (o gerenciador de pacotes) está configurado para usar o repositório backport. A não ser que você saiba o que você esteá fazendo, desencorajamos fortemente a instalação de pacotes de backports porque é provável que crie instabilidades ou conflitos no seu sistema.",
+ "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o domínio '{domain}'",
+ "certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado."
}
diff --git a/locales/ru.json b/locales/ru.json
index afe8e06f0..5a74524bf 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -1,33 +1,33 @@
{
- "action_invalid": "Неверное действие '{action:s}'",
+ "action_invalid": "Неверное действие '{action}'",
"admin_password": "Пароль администратора",
"admin_password_change_failed": "Невозможно изменить пароль",
"admin_password_changed": "Пароль администратора был изменен",
- "app_already_installed": "{app:s} уже установлено",
+ "app_already_installed": "{app} уже установлено",
"app_already_installed_cant_change_url": "Это приложение уже установлено. URL не может быть изменен только с помощью этой функции. Изучите `app changeurl`, если это доступно.",
- "app_argument_choice_invalid": "Неверный выбор для аргумента '{name:s}', Это должно быть '{choices:s}'",
- "app_argument_invalid": "Недопустимое значение аргумента '{name:s}': {error:s}'",
- "app_already_up_to_date": "{app:s} уже обновлено",
- "app_argument_required": "Аргумент '{name:s}' необходим",
- "app_change_url_identical_domains": "Старый и новый domain/url_path идентичны ('{domain:s}{path:s}'), ничего делать не надо.",
- "app_change_url_no_script": "Приложение '{app_name:s}' не поддерживает изменение url. Наверное, вам нужно обновить приложение.",
- "app_change_url_success": "Успешно изменён {app:s} url на {domain:s}{path:s}",
+ "app_argument_choice_invalid": "Неверный выбор для аргумента '{name}', Это должно быть '{choices}'",
+ "app_argument_invalid": "Недопустимое значение аргумента '{name}': {error}'",
+ "app_already_up_to_date": "{app} уже обновлено",
+ "app_argument_required": "Аргумент '{name}' необходим",
+ "app_change_url_identical_domains": "Старый и новый domain/url_path идентичны ('{domain}{path}'), ничего делать не надо.",
+ "app_change_url_no_script": "Приложение '{app_name}' не поддерживает изменение url. Наверное, вам нужно обновить приложение.",
+ "app_change_url_success": "Успешно изменён {app} url на {domain}{path}",
"app_extraction_failed": "Невозможно извлечь файлы для инсталляции",
"app_id_invalid": "Неправильный id приложения",
"app_install_files_invalid": "Неправильные файлы инсталляции",
- "app_location_unavailable": "Этот url отсутствует или конфликтует с уже установленным приложением или приложениями: {apps:s}",
+ "app_location_unavailable": "Этот url отсутствует или конфликтует с уже установленным приложением или приложениями: {apps}",
"app_manifest_invalid": "Недопустимый манифест приложения: {error}",
- "app_not_correctly_installed": "{app:s} , кажется, установлены неправильно",
- "app_not_installed": "{app:s} не установлены",
- "app_not_properly_removed": "{app:s} удалены неправильно",
- "app_removed": "{app:s} удалено",
+ "app_not_correctly_installed": "{app} , кажется, установлены неправильно",
+ "app_not_installed": "{app} не установлены",
+ "app_not_properly_removed": "{app} удалены неправильно",
+ "app_removed": "{app} удалено",
"app_requirements_checking": "Проверяю необходимые пакеты для {app}...",
"app_sources_fetch_failed": "Невозможно получить исходные файлы",
"app_unknown": "Неизвестное приложение",
"app_upgrade_app_name": "Обновление приложения {app}...",
- "app_upgrade_failed": "Невозможно обновить {app:s}",
+ "app_upgrade_failed": "Невозможно обновить {app}",
"app_upgrade_some_app_failed": "Невозможно обновить некоторые приложения",
- "app_upgraded": "{app:s} обновлено",
+ "app_upgraded": "{app} обновлено",
"installation_complete": "Установка завершена",
"password_too_simple_1": "Пароль должен быть не менее 8 символов"
}
\ No newline at end of file
diff --git a/locales/sv.json b/locales/sv.json
index 26162419e..39707d07c 100644
--- a/locales/sv.json
+++ b/locales/sv.json
@@ -5,7 +5,7 @@
"admin_password": "Administratörslösenord",
"admin_password_too_long": "Välj gärna ett lösenord som inte innehåller fler än 127 tecken",
"admin_password_change_failed": "Kan inte byta lösenord",
- "action_invalid": "Ej tillåten åtgärd '{action:s}'",
+ "action_invalid": "Ej tillåten åtgärd '{action}'",
"admin_password_changed": "Administratörskontots lösenord ändrades",
"aborting": "Avbryter."
}
\ No newline at end of file
diff --git a/locales/uk.json b/locales/uk.json
new file mode 100644
index 000000000..b54d81fbd
--- /dev/null
+++ b/locales/uk.json
@@ -0,0 +1,713 @@
+{
+ "app_manifest_install_ask_domain": "Оберіть домен, в якому треба встановити цей застосунок",
+ "app_manifest_invalid": "Щось не так з маніфестом застосунку: {error}",
+ "app_location_unavailable": "Ця URL-адреса або недоступна, або конфліктує з уже встановленим застосунком (застосунками):\n{apps}",
+ "app_label_deprecated": "Ця команда застаріла! Будь ласка, використовуйте нову команду 'yunohost user permission update' для управління заголовком застосунку.",
+ "app_make_default_location_already_used": "Неможливо зробити '{app}' типовим застосунком на домені, '{domain}' вже використовується '{other_app}'",
+ "app_install_script_failed": "Сталася помилка в скрипті встановлення застосунку",
+ "app_install_failed": "Неможливо встановити {app}: {error}",
+ "app_install_files_invalid": "Ці файли не можуть бути встановлені",
+ "app_id_invalid": "Неприпустимий ID застосунку",
+ "app_full_domain_unavailable": "Вибачте, цей застосунок повинен бути встановлений на власному домені, але інші застосунки вже встановлені на домені '{domain}'. Замість цього ви можете використовувати піддомен, призначений для цього застосунку.",
+ "app_extraction_failed": "Не вдалося витягти файли встановлення",
+ "app_change_url_success": "URL-адреса {app} тепер {domain}{path}",
+ "app_change_url_no_script": "Застосунок '{app_name}' поки не підтримує зміну URL-адрес. Можливо, вам слід оновити його.",
+ "app_change_url_identical_domains": "Старий і новий domain/url_path збігаються ('{domain}{path}'), нічого робити не треба.",
+ "app_argument_required": "Аргумент '{name}' необхідний",
+ "app_argument_password_no_default": "Помилка під час розбору аргументу пароля '{name}': аргумент пароля не може мати типове значення з причин безпеки",
+ "app_argument_invalid": "Виберіть правильне значення для аргументу '{name}': {error}",
+ "app_argument_choice_invalid": "Виберіть дійсне значення для аргументу '{name}': '{value}' не є серед доступних варіантів ({choices})",
+ "app_already_up_to_date": "{app} має найостаннішу версію",
+ "app_already_installed_cant_change_url": "Цей застосунок уже встановлено. URL-адреса не може бути змінена тільки цією функцією. Перевірте в `app changeurl`, якщо вона доступна.",
+ "app_already_installed": "{app} уже встановлено",
+ "app_action_broke_system": "Ця дія, схоже, порушила роботу наступних важливих служб: {services}",
+ "app_action_cannot_be_ran_because_required_services_down": "Для виконання цієї дії повинні бути запущені наступні необхідні служби: {services}. Спробуйте перезапустити їх, щоб продовжити (і, можливо, з'ясувати, чому вони не працюють).",
+ "already_up_to_date": "Нічого не потрібно робити. Все вже актуально.",
+ "admin_password_too_long": "Будь ласка, виберіть пароль коротше 127 символів",
+ "admin_password_changed": "Пароль адміністрації було змінено",
+ "admin_password_change_failed": "Неможливо змінити пароль",
+ "admin_password": "Пароль адміністрації",
+ "additional_urls_already_removed": "Додаткова URL-адреса '{url}' вже видалена в додатковій URL-адресі для дозволу '{permission}'",
+ "additional_urls_already_added": "Додаткова URL-адреса '{url}' вже додана в додаткову URL-адресу для дозволу '{permission}'",
+ "action_invalid": "Неприпустима дія '{action}'",
+ "aborting": "Переривання.",
+ "diagnosis_description_web": "Мережа",
+ "service_reloaded_or_restarted": "Службу '{service}' була перезавантажено або перезапущено",
+ "service_reload_or_restart_failed": "Не вдалося перезавантажити або перезапустити службу '{service}' \n\nНедавні журнали служби: {logs}",
+ "service_restarted": "Службу '{service}' перезапущено",
+ "service_restart_failed": "Не вдалося запустити службу '{service}' \n\nНедавні журнали служб: {logs}",
+ "service_reloaded": "Служба '{service}' перезавантажена",
+ "service_reload_failed": "Не вдалося перезавантажити службу '{service}'\n\nОстанні журнали служби: {logs}",
+ "service_removed": "Служба '{service}' вилучена",
+ "service_remove_failed": "Не вдалося видалити службу '{service}'",
+ "service_regen_conf_is_deprecated": "'yunohost service regen-conf' застарів! Будь ласка, використовуйте 'yunohost tools regen-conf' замість цього.",
+ "service_enabled": "Служба '{service}' тепер буде автоматично запускатися під час завантаження системи.",
+ "service_enable_failed": "Неможливо змусити службу '{service}' автоматично запускатися під час завантаження.\n\nНедавні журнали служби: {logs}",
+ "service_disabled": "Служба '{service}' більше не буде запускатися під час завантаження системи.",
+ "service_disable_failed": "Неможливо змусити службу '{service}' не запускатися під час завантаження.\n\nОстанні журнали служби: {logs}",
+ "service_description_yunohost-firewall": "Управляє відкритими і закритими портами з'єднання зі службами",
+ "service_description_yunohost-api": "Управляє взаємодією між вебінтерфейсом YunoHost і системою",
+ "service_description_ssh": "Дозволяє віддалено під'єднуватися до сервера через термінал (протокол SSH)",
+ "service_description_slapd": "Зберігає користувачів, домени і пов'язані з ними дані",
+ "service_description_rspamd": "Фільтри спаму і інші функції, пов'язані з е-поштою",
+ "service_description_redis-server": "Спеціалізована база даних, яка використовується для швидкого доступу до даних, черги завдань і зв'язку між програмами",
+ "service_description_postfix": "Використовується для надсилання та отримання е-пошти",
+ "service_description_php7.3-fpm": "Запускає застосунки, написані мовою програмування PHP за допомогою NGINX",
+ "service_description_nginx": "Обслуговує або надає доступ до всіх вебсайтів, розміщених на вашому сервері",
+ "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": "Не вдалося виконати команду '{command}'",
+ "service_already_stopped": "Службу '{service}' вже зупинено",
+ "service_already_started": "Службу '{service}' вже запущено",
+ "service_added": "Службу '{service}' було додано",
+ "service_add_failed": "Не вдалося додати службу '{service}'",
+ "server_reboot_confirm": "Сервер буде негайно перезавантажено, ви впевнені? [{answers}]",
+ "server_reboot": "Сервер буде перезавантажено",
+ "server_shutdown_confirm": "Сервер буде негайно вимкнено, ви впевнені? [{answers}]",
+ "server_shutdown": "Сервер буде вимкнено",
+ "root_password_replaced_by_admin_password": "Ваш кореневий (root) пароль було замінено на пароль адміністратора.",
+ "root_password_desynchronized": "Пароль адміністратора було змінено, але YunoHost не зміг поширити це на кореневий (root) пароль!",
+ "restore_system_part_failed": "Не вдалося відновити системний розділ '{part}'",
+ "restore_running_hooks": "Запуск хуків відновлення…",
+ "restore_running_app_script": "Відновлення застосунку '{app}'…",
+ "restore_removing_tmp_dir_failed": "Неможливо видалити старий тимчасовий каталог",
+ "restore_nothings_done": "Нічого не було відновлено",
+ "restore_not_enough_disk_space": "Недостатньо місця (простір: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin: d} Б)",
+ "restore_may_be_not_enough_disk_space": "Схоже, у вашій системі недостатньо місця (вільно: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin: d} Б)",
+ "restore_hook_unavailable": "Скрипт відновлення для '{part}' недоступний у вашій системі і в архіві його теж немає",
+ "restore_failed": "Не вдалося відновити систему",
+ "restore_extracting": "Витягнення необхідних файлів з архіву…",
+ "restore_confirm_yunohost_installed": "Ви дійсно хочете відновити вже встановлену систему? [{answers}]",
+ "restore_complete": "Відновлення завершено",
+ "restore_cleaning_failed": "Не вдалося очистити тимчасовий каталог відновлення",
+ "restore_backup_too_old": "Цей архів резервних копій не може бути відновлений, бо він отриманий з дуже старої версії YunoHost.",
+ "restore_already_installed_apps": "Наступні програми не можуть бути відновлені, тому що вони вже встановлені: {apps}",
+ "restore_already_installed_app": "Застосунок з ID «{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": "Застосування очікує конфігурації для категорії '{category}'...",
+ "regenconf_failed": "Не вдалося відновити конфігурацію для категорії (категорій): {categories}",
+ "regenconf_dry_pending_applying": "Перевірка очікує конфігурації, яка була б застосована для категорії '{category}'…",
+ "regenconf_would_be_updated": "Конфігурація була б оновлена для категорії '{category}'",
+ "regenconf_updated": "Конфігурація оновлена для категорії '{category}'",
+ "regenconf_up_to_date": "Конфігурація вже оновлена для категорії '{category}'",
+ "regenconf_now_managed_by_yunohost": "Конфігураційний файл '{conf}' тепер управляється YunoHost (категорія {category}).",
+ "regenconf_file_updated": "Конфігураційний файл '{conf}' оновлено",
+ "regenconf_file_removed": "Конфігураційний файл '{conf}' видалено",
+ "regenconf_file_remove_failed": "Неможливо видалити файл конфігурації '{conf}'",
+ "regenconf_file_manually_removed": "Конфігураційний файл '{conf}' було видалено вручну і не буде створено",
+ "regenconf_file_manually_modified": "Конфігураційний файл '{conf}' було змінено вручну і не буде оновлено",
+ "regenconf_file_kept_back": "Очікувалося видалення конфігураційного файлу '{conf}' за допомогою regen-conf (категорія {category}), але його було збережено.",
+ "regenconf_file_copy_failed": "Не вдалося скопіювати новий файл конфігурації '{new}' в '{conf}'",
+ "regenconf_file_backed_up": "Конфігураційний файл '{conf}' збережено в '{backup}'",
+ "postinstall_low_rootfsspace": "Загальне місце кореневої файлової системи становить менше 10 ГБ, що викликає занепокоєння! Швидше за все, дисковий простір закінчиться дуже скоро! Рекомендовано мати не менше 16 ГБ для кореневої файлової системи. Якщо ви хочете встановити YunoHost попри це попередження, повторно запустіть післявстановлення з параметром --force-diskspace",
+ "port_already_opened": "Порт {port} вже відкрито для з'єднань {ip_version}",
+ "port_already_closed": "Порт {port} вже закрито для з'єднань {ip_version}",
+ "permission_require_account": "Дозвіл {permission} має зміст тільки для користувачів, що мають обліковий запис, і тому не може бути увімкненим для відвідувачів.",
+ "permission_protected": "Дозвіл {permission} захищено. Ви не можете додавати або вилучати групу відвідувачів до/з цього дозволу.",
+ "permission_updated": "Дозвіл '{permission}' оновлено",
+ "permission_update_failed": "Не вдалося оновити дозвіл '{permission}': {error}",
+ "permission_not_found": "Дозвіл '{permission}' не знайдено",
+ "permission_deletion_failed": "Не вдалося видалити дозвіл '{permission}': {error}",
+ "permission_deleted": "Дозвіл '{permission}' видалено",
+ "permission_cant_add_to_all_users": "Дозвіл {permission} не може бути додано всім користувачам.",
+ "permission_currently_allowed_for_all_users": "Наразі цей дозвіл надається всім користувачам на додачу до інших груп. Імовірно, вам потрібно або видалити дозвіл 'all_users', або видалити інші групи, яким його зараз надано.",
+ "permission_creation_failed": "Не вдалося створити дозвіл '{permission}': {error}",
+ "permission_created": "Дозвіл '{permission}' створено",
+ "permission_cannot_remove_main": "Вилучення основного дозволу заборонене",
+ "permission_already_up_to_date": "Дозвіл не було оновлено, тому що запити на додавання/вилучення вже відповідають поточному стану.",
+ "permission_already_exist": "Дозвіл '{permission}' вже існує",
+ "permission_already_disallowed": "Група '{group}' вже має вимкнений дозвіл '{permission}'",
+ "permission_already_allowed": "Група '{group}' вже має увімкнений дозвіл '{permission}'",
+ "pattern_password_app": "На жаль, паролі не можуть містити такі символи: {forbidden_chars}",
+ "pattern_username": "Має складатися тільки з букв і цифр в нижньому регістрі і символів підкреслення",
+ "pattern_port_or_range": "Має бути припустимий номер порту (наприклад, 0-65535) або діапазон портів (наприклад, 100:200)",
+ "pattern_password": "Має бути довжиною не менше 3 символів",
+ "pattern_mailbox_quota": "Має бути розмір з суфіксом b/k/M/G/T або 0, щоб не мати квоти",
+ "pattern_lastname": "Має бути припустиме прізвище",
+ "pattern_firstname": "Має бути припустиме ім'я",
+ "pattern_email": "Має бути припустима адреса е-пошти, без символу '+' (наприклад, someone@example.com)",
+ "pattern_email_forward": "Має бути припустима адреса е-пошти, символ '+' приймається (наприклад, someone+tag@example.com)",
+ "pattern_domain": "Має бути припустиме доменне ім'я (наприклад, my-domain.org)",
+ "pattern_backup_archive_name": "Має бути правильна назва файлу, що містить не більше 30 символів, тільки букви і цифри і символи -_",
+ "password_too_simple_4": "Пароль має складатися не менше ніж з 12 символів і містити цифри, великі та малі символи і спеціальні символи",
+ "password_too_simple_3": "Пароль має складатися не менше ніж з 8 символів і містити цифри, великі та малі символи і спеціальні символи",
+ "password_too_simple_2": "Пароль має складатися не менше ніж з 8 символів і містити цифри, великі та малі символи",
+ "password_too_simple_1": "Пароль має складатися не менше ніж з 8 символів",
+ "password_listed": "Цей пароль входить в число найбільш часто використовуваних паролів у світі. Будь ласка, виберіть щось неповторюваніше.",
+ "packages_upgrade_failed": "Не вдалося оновити всі пакети",
+ "operation_interrupted": "Операція була вручну перервана?",
+ "invalid_number": "Має бути числом",
+ "not_enough_disk_space": "Недостатньо вільного місця на '{path}'",
+ "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадміністрації або виконайте команду `yunohost tools migrations run`.",
+ "migrations_success_forward": "Міграцію {id} завершено",
+ "migrations_skip_migration": "Пропускання міграції {id}...",
+ "migrations_running_forward": "Виконання міграції {id}...",
+ "migrations_pending_cant_rerun": "Наступні міграції ще не завершені, тому не можуть бути запущені знову: {ids}",
+ "migrations_not_pending_cant_skip": "Наступні міграції не очікують виконання, тому не можуть бути пропущені: {ids}",
+ "migrations_no_such_migration": "Не існує міграції під назвою '{id}'",
+ "migrations_no_migrations_to_run": "Немає міграцій для запуску",
+ "migrations_need_to_accept_disclaimer": "Щоб запустити міграцію {id}, ви повинні прийняти наступну відмову від відповідальності:\n---\n{disclaimer}\n---\nЯкщо ви згодні запустити міграцію, будь ласка, повторіть команду з опцією '--accept-disclaimer'.",
+ "migrations_must_provide_explicit_targets": "Ви повинні вказати явні цілі при використанні '--skip' або '--force-rerun'",
+ "migrations_migration_has_failed": "Міграція {id} не завершена, перериваємо. Помилка: {exception}",
+ "migrations_loading_migration": "Завантаження міграції {id}...",
+ "migrations_list_conflict_pending_done": "Ви не можете одночасно використовувати '--previous' і '--done'.",
+ "migrations_exclusive_options": "'--auto', '--skip', і '--force-rerun' є взаємовиключними опціями.",
+ "migrations_failed_to_load_migration": "Не вдалося завантажити міграцію {id}: {error}",
+ "migrations_dependencies_not_satisfied": "Запустіть ці міграції: '{dependencies_id}', перед міграцією {id}.",
+ "migrations_cant_reach_migration_file": "Не вдалося отримати доступ до файлів міграцій за шляхом '%s'",
+ "migrations_already_ran": "Наступні міграції вже виконано: {ids}",
+ "migration_0019_slapd_config_will_be_overwritten": "Схоже, що ви вручну відредагували конфігурацію slapd. Для цього критичного переходу YunoHost повинен примусово оновити конфігурацію slapd. Оригінальні файли будуть збережені в {conf_backup_folder}.",
+ "migration_0019_add_new_attributes_in_ldap": "Додавання нових атрибутів для дозволів у базі даних LDAP",
+ "migration_0018_failed_to_reset_legacy_rules": "Не вдалося скинути спадкові правила iptables: {error}",
+ "migration_0018_failed_to_migrate_iptables_rules": "Не вдалося перенести спадкові правила iptables в nftables: {error}",
+ "migration_0017_not_enough_space": "Звільніть достатньо місця в {path} для запуску міграції.",
+ "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 встановлено, але не PostgreSQL 11‽ Можливо, у вашій системі відбулося щось дивне :(...",
+ "migration_0017_postgresql_96_not_installed": "PostgreSQL не встановлено у вашій системі. Нічого не потрібно робити.",
+ "migration_0015_weak_certs": "Було виявлено, що такі сертифікати все ще використовують слабкі алгоритми підпису і повинні бути оновлені для сумісності з наступною версією nginx: {certs}",
+ "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 зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частина системи або її застосунків.\n\nТому рекомендовано:\n - Виконати резервне копіювання всіх важливих даних або застосунків. Подробиці на сайті https://yunohost.org/backup; \n - Наберіться терпіння після запуску міграції: В залежності від вашого з'єднання з Інтернетом і апаратного забезпечення, оновлення може зайняти до декількох годин.",
+ "migration_0015_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Buster.",
+ "migration_0015_not_enough_free_space": "Вільного місця в /var/ досить мало! У вас повинно бути не менше 1 ГБ вільного місця, щоб запустити цю міграцію.",
+ "migration_0015_not_stretch": "Поточний дистрибутив Debian не є Stretch!",
+ "migration_0015_yunohost_upgrade": "Початок оновлення ядра YunoHost...",
+ "migration_0015_still_on_stretch_after_main_upgrade": "Щось пішло не так під час основного оновлення, система, схоже, все ще знаходиться на Debian Stretch",
+ "migration_0015_main_upgrade": "Початок основного оновлення...",
+ "migration_0015_patching_sources_list": "Виправлення sources.lists...",
+ "migration_0015_start": "Початок міграції на Buster",
+ "migration_update_LDAP_schema": "Оновлення схеми LDAP...",
+ "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_description_0020_ssh_sftp_permissions": "Додавання підтримки дозволів SSH і SFTP",
+ "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",
+ "migration_description_0015_migrate_to_buster": "Оновлення системи до Debian Buster і YunoHost 4.x",
+ "migrating_legacy_permission_settings": "Перенесення спадкових налаштувань дозволів...",
+ "main_domain_changed": "Основний домен було змінено",
+ "main_domain_change_failed": "Неможливо змінити основний домен",
+ "mail_unavailable": "Ця е-пошта зарезервована і буде автоматично виділена найпершому користувачеві",
+ "mailbox_used_space_dovecot_down": "Поштова служба Dovecot повинна бути запущена, якщо ви хочете отримати використане місце в поштовій скриньці",
+ "mailbox_disabled": "Е-пошта вимкнена для користувача {user}",
+ "mail_forward_remove_failed": "Не вдалося видалити переадресацію електронної пошти '{mail}'",
+ "mail_domain_unknown": "Неправильна адреса е-пошти для домену '{domain}'. Будь ласка, використовуйте домен, що адмініструється цим сервером.",
+ "mail_alias_remove_failed": "Не вдалося видалити аліас електронної пошти '{mail}'",
+ "log_tools_reboot": "Перезавантаження сервера",
+ "log_tools_shutdown": "Вимикання сервера",
+ "log_tools_upgrade": "Оновлення системних пакетів",
+ "log_tools_postinstall": "Післявстановлення сервера YunoHost",
+ "log_tools_migrations_migrate_forward": "Запущено міграції",
+ "log_domain_main_domain": "Зроблено '{}' основним доменом",
+ "log_user_permission_reset": "Скинуто дозвіл «{}»",
+ "log_user_permission_update": "Оновлено доступи для дозволу '{}'",
+ "log_user_update": "Оновлено відомості для користувача '{}'",
+ "log_user_group_update": "Оновлено групу '{}'",
+ "log_user_group_delete": "Видалено групу «{}»",
+ "log_user_group_create": "Створено групу '{}'",
+ "log_user_delete": "Видалення користувача '{}'",
+ "log_user_create": "Додавання користувача '{}'",
+ "log_regen_conf": "Перестворення системних конфігурацій '{}'",
+ "log_letsencrypt_cert_renew": "Оновлення сертифікату Let's Encrypt на домені '{}'",
+ "log_selfsigned_cert_install": "Установлення самопідписаного сертифікату на домені '{}'",
+ "log_permission_url": "Оновлення URL, пов'язаногл з дозволом '{}'",
+ "log_permission_delete": "Видалення дозволу '{}'",
+ "log_permission_create": "Створення дозволу '{}'",
+ "log_letsencrypt_cert_install": "Установлення сертифікату Let's Encrypt на домен '{}'",
+ "log_dyndns_update": "Оновлення IP, пов'язаного з вашим піддоменом YunoHost '{}'",
+ "log_dyndns_subscribe": "Підписка на піддомен YunoHost '{}'",
+ "log_domain_remove": "Вилучення домену '{}' з конфігурації системи",
+ "log_domain_add": "Додавання домену '{}' в конфігурацію системи",
+ "log_remove_on_failed_install": "Вилучення '{}' після невдалого встановлення",
+ "log_remove_on_failed_restore": "Вилучення '{}' після невдалого відновлення з резервного архіву",
+ "log_backup_restore_app": "Відновлення '{}' з архіву резервних копій",
+ "log_backup_restore_system": "Відновлення системи з резервного архіву",
+ "log_backup_create": "Створення резервного архіву",
+ "log_available_on_yunopaste": "Цей журнал тепер доступний за посиланням {url}",
+ "log_app_action_run": "Запуск дії застосунку «{}»",
+ "log_app_makedefault": "Застосунок '{}' зроблено типовим",
+ "log_app_upgrade": "Оновлення застосунку '{}'",
+ "log_app_remove": "Вилучення застосунку '{}'",
+ "log_app_install": "Установлення застосунку '{}'",
+ "log_app_change_url": "Змінення URL-адреси застосунку «{}»",
+ "log_operation_unit_unclosed_properly": "Блок операцій не був закритий належним чином",
+ "log_does_exists": "Немає журналу операцій з назвою '{log}', використовуйте 'yunohost log list', щоб подивитися всі доступні журнали операцій",
+ "log_help_to_get_failed_log": "Операція '{desc}' не може бути завершена. Будь ласка, поділіться повним журналом цієї операції, використовуючи команду 'yunohost log share {name}', щоб отримати допомогу",
+ "log_link_to_failed_log": "Не вдалося завершити операцію '{desc}'. Будь ласка, надайте повний журнал цієї операції, натиснувши тут, щоб отримати допомогу",
+ "log_help_to_get_log": "Щоб переглянути журнал операції '{desc}', використовуйте команду 'yunohost log show {name}'",
+ "log_link_to_log": "Повний журнал цієї операції: '{desc}'",
+ "log_corrupted_md_file": "Файл метаданих YAML, пов'язаний з журналами, пошкоджено: '{md_file}\nПомилка: {error}'",
+ "iptables_unavailable": "Ви не можете грати з iptables тут. Ви перебуваєте або в контейнері, або ваше ядро не підтримує його",
+ "ip6tables_unavailable": "Ви не можете грати з ip6tables тут. Ви перебуваєте або в контейнері, або ваше ядро не підтримує його",
+ "invalid_regex": "Неприпустимий regex: '{regex}'",
+ "installation_complete": "Установлення завершено",
+ "hook_name_unknown": "Невідома назва хука '{name}'",
+ "hook_list_by_invalid": "Цю властивість не може бути використано для перерахування хуків (гачків)",
+ "hook_json_return_error": "Не вдалося розпізнати повернення з хука {path}. Помилка: {msg}. Необроблений контент: {raw_content}",
+ "hook_exec_not_terminated": "Скрипт не завершився належним чином: {path}",
+ "hook_exec_failed": "Не вдалося запустити скрипт: {path}",
+ "group_user_not_in_group": "Користувач {user} не входить в групу {group}",
+ "group_user_already_in_group": "Користувач {user} вже в групі {group}",
+ "group_update_failed": "Не вдалося оновити групу '{group}': {error}",
+ "group_updated": "Групу '{group}' оновлено",
+ "group_unknown": "Група '{group}' невідома",
+ "group_deletion_failed": "Не вдалося видалити групу '{group}': {error}",
+ "group_deleted": "Групу '{group}' видалено",
+ "group_cannot_be_deleted": "Група {group} не може бути видалена вручну.",
+ "group_cannot_edit_primary_group": "Група '{group}' не може бути відредагована вручну. Це основна група, призначена тільки для одного конкретного користувача.",
+ "group_cannot_edit_visitors": "Група 'visitors' не може бути відредагована вручну. Це спеціальна група, що представляє анонімних відвідувачів",
+ "group_cannot_edit_all_users": "Група 'all_users' не може бути відредагована вручну. Це спеціальна група, призначена для всіх користувачів, зареєстрованих в YunoHost",
+ "group_creation_failed": "Не вдалося створити групу '{group}': {error}",
+ "group_created": "Групу '{group}' створено",
+ "group_already_exist_on_system_but_removing_it": "Група {group} вже існує в групах системи, але YunoHost вилучить її...",
+ "group_already_exist_on_system": "Група {group} вже існує в групах системи",
+ "group_already_exist": "Група {group} вже існує",
+ "good_practices_about_user_password": "Зараз ви збираєтеся поставити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).",
+ "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрації. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).",
+ "global_settings_unknown_type": "Несподівана ситуація, налаштування {setting} має тип {unknown_type}, але це не тип, підтримуваний системою.",
+ "global_settings_setting_backup_compress_tar_archives": "При створенні нових резервних копій стискати архіви (.tar.gz) замість нестислих архівів (.tar). NB: вмикання цієї опції означає створення легших архівів резервних копій, але початкова процедура резервного копіювання буде значно довшою і важчою для CPU.",
+ "global_settings_setting_security_webadmin_allowlist": "IP-адреси, яким дозволений доступ до вебадміністрації. Через кому.",
+ "global_settings_setting_security_webadmin_allowlist_enabled": "Дозволити доступ до вебадміністрації тільки деяким IP-адресам.",
+ "global_settings_setting_smtp_relay_password": "Пароль хоста SMTP-ретрансляції",
+ "global_settings_setting_smtp_relay_user": "Обліковий запис користувача SMTP-ретрансляції",
+ "global_settings_setting_smtp_relay_port": "Порт SMTP-ретрансляції",
+ "global_settings_setting_smtp_relay_host": "Хост SMTP-ретрансляції, який буде використовуватися для надсилання е-пошти замість цього зразка Yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний DNS або цей сервер не доступний безпосередньо в Інтернеті і ви хочете використовувати інший сервер для відправки електронних листів.",
+ "global_settings_setting_smtp_allow_ipv6": "Дозволити використання IPv6 для отримання і надсилання листів е-пошти",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "Увімкнути накладення панелі SSOwat",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Дозволити використання (застарілого) ключа DSA для конфігурації демона SSH",
+ "global_settings_unknown_setting_from_settings_file": "Невідомий ключ в налаштуваннях: '{setting_key}', відхиліть його і збережіть у /etc/yunohost/settings-unknown.json",
+ "global_settings_setting_security_ssh_port": "SSH-порт",
+ "global_settings_setting_security_postfix_compatibility": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)",
+ "global_settings_setting_security_ssh_compatibility": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)",
+ "global_settings_setting_security_password_user_strength": "Надійність пароля користувача",
+ "global_settings_setting_security_password_admin_strength": "Надійність пароля адміністратора",
+ "global_settings_setting_security_nginx_compatibility": "Компроміс між сумісністю і безпекою для вебсервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)",
+ "global_settings_setting_pop3_enabled": "Увімкніть протокол POP3 для поштового сервера",
+ "global_settings_reset_success": "Попередні налаштування тепер збережені в {path}",
+ "global_settings_key_doesnt_exists": "Ключ '{settings_key}' не існує в глобальних налаштуваннях, ви можете побачити всі доступні ключі, виконавши команду 'yunohost settings list'",
+ "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": "Файл {path} не існує.",
+ "field_invalid": "Неприпустиме поле '{}'",
+ "experimental_feature": "Попередження: Ця функція є експериментальною і не вважається стабільною, ви не повинні використовувати її, якщо не знаєте, що робите.",
+ "extracting": "Витягнення...",
+ "dyndns_unavailable": "Домен '{domain}' недоступний.",
+ "dyndns_domain_not_provided": "DynDNS провайдер {provider} не може надати домен {domain}.",
+ "dyndns_registration_failed": "Не вдалося зареєструвати домен DynDNS: {error}",
+ "dyndns_registered": "Домен DynDNS зареєстровано",
+ "dyndns_provider_unreachable": "Неможливо зв'язатися з провайдером DynDNS {provider}: або ваш YunoHost неправильно під'єднано до Інтернету, або сервер dynette не працює.",
+ "dyndns_no_domain_registered": "Домен не зареєстровано в DynDNS",
+ "dyndns_key_not_found": "DNS-ключ для домену не знайдено",
+ "dyndns_key_generating": "Утворення DNS-ключа... Це може зайняти деякий час.",
+ "dyndns_ip_updated": "Вашу IP-адресу в DynDNS оновлено",
+ "dyndns_ip_update_failed": "Не вдалося оновити IP-адресу в DynDNS",
+ "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": "Завантаження…",
+ "done": "Готово",
+ "domains_available": "Доступні домени:",
+ "domain_name_unknown": "Домен '{domain}' невідомий",
+ "domain_uninstall_app_first": "Ці застосунки все ще встановлені на вашому домені:\n{apps}\n\nВидаліть їх за допомогою 'yunohost app remove the_app_id' або перемістіть їх на інший домен за допомогою 'yunohost app change-url the_app_id', перш ніж приступити до вилучення домену",
+ "domain_remove_confirm_apps_removal": "Вилучення цього домену призведе до вилучення таких застосунків:\n{apps}\n\nВи впевнені, що хочете це зробити? [{answers}]",
+ "domain_hostname_failed": "Неможливо встановити нову назву хоста. Це може викликати проблеми в подальшому (можливо, все буде в порядку).",
+ "domain_exists": "Цей домен уже існує",
+ "domain_dyndns_root_unknown": "Невідомий кореневий домен DynDNS",
+ "domain_dyndns_already_subscribed": "Ви вже підписалися на домен DynDNS",
+ "domain_dns_conf_is_just_a_recommendation": "Ця команда показує *рекомендовану* конфігурацію. Насправді вона не встановлює конфігурацію DNS для вас. Ви самі повинні налаштувати свою зону DNS у реєстратора відповідно до цих рекомендацій.",
+ "domain_deletion_failed": "Неможливо видалити домен {domain}: {error}",
+ "domain_deleted": "Домен видалено",
+ "domain_creation_failed": "Неможливо створити домен {domain}: {error}",
+ "domain_created": "Домен створено",
+ "domain_cert_gen_failed": "Не вдалося утворити сертифікат",
+ "domain_cannot_remove_main_add_new_one": "Ви не можете видалити '{domain}', так як це основний домен і ваш єдиний домен, вам потрібно спочатку додати інший домен за допомогою 'yunohost domain add ', потім встановити його як основний домен за допомогою 'yunohost domain main-domain -n ' і потім ви можете вилучити домен '{domain}' за допомогою 'yunohost domain remove {domain}'.'",
+ "domain_cannot_add_xmpp_upload": "Ви не можете додавати домени, що починаються з 'xmpp-upload.'. Таку назву зарезервовано для функції XMPP upload, вбудованої в YunoHost.",
+ "domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}",
+ "disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку",
+ "disk_space_not_sufficient_install": "Недостатньо місця на диску для встановлення цього застосунку",
+ "diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.port -v YOUR_SSH_PORT , щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force , щоб скинути ваш конфіг на рекомендований YunoHost.",
+ "diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.port', що дозволяє уникнути ручного редагування конфігурації.",
+ "diagnosis_sshd_config_insecure": "Схоже, що конфігурація SSH була змінена вручну і є небезпечною, оскільки не містить директив 'AllowGroups' або 'AllowUsers' для обмеження доступу авторизованих користувачів.",
+ "diagnosis_processes_killed_by_oom_reaper": "Деякі процеси було недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів:\n{kills_summary}",
+ "diagnosis_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з вебадміністрації, або використовуючи 'yunohost diagnosis run' з командного рядка.",
+ "diagnosis_unknown_categories": "Наступні категорії невідомі: {categories}",
+ "diagnosis_http_nginx_conf_not_up_to_date_details": "Щоб виправити становище, перевірте різницю за допомогою командного рядка, використовуючи yunohost tools regen-conf nginx --dry-run --with-diff , і якщо все в порядку, застосуйте зміни за допомогою команди yunohost tools regen-conf nginx --force .",
+ "diagnosis_http_nginx_conf_not_up_to_date": "Схоже, що конфігурація nginx цього домену була змінена вручну, що не дозволяє YunoHost визначити, чи доступний він по HTTP.",
+ "diagnosis_http_partially_unreachable": "Домен {domain} здається недоступним по HTTP поза локальною мережею в IPv{failed}, хоча він працює в IPv{passed}.",
+ "diagnosis_http_unreachable": "Домен {domain} здається недоступним через HTTP поза локальною мережею.",
+ "diagnosis_http_bad_status_code": "Схоже, що замість вашого сервера відповіла інша машина (можливо, ваш маршрутизатор).
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлено на ваш сервер .
2. На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.",
+ "diagnosis_http_connection_error": "Помилка з'єднання: не вдалося з'єднатися із запитуваним доменом, швидше за все, він недоступний.",
+ "diagnosis_http_timeout": "При спробі зв'язатися з вашим сервером ззовні стався тайм-аут. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлено на ваш сервер .
2. Ви також повинні переконатися, що служба nginx запущена
3.На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.",
+ "diagnosis_http_ok": "Домен {domain} доступний по HTTP поза локальною мережею.",
+ "diagnosis_http_localdomain": "Домен {domain} з .local TLD не може бути доступний ззовні локальної мережі.",
+ "diagnosis_http_could_not_diagnose_details": "Помилка: {error}",
+ "diagnosis_http_could_not_diagnose": "Не вдалося діагностувати досяжність доменів ззовні в IPv{ipversion}.",
+ "diagnosis_http_hairpinning_issue_details": "Можливо, це пов'язано з коробкою/маршрутизатором вашого інтернет-провайдера. В результаті, люди ззовні вашої локальної мережі зможуть отримати доступ до вашого сервера, як і очікувалося, але не люди зсередини локальної мережі (як ви, ймовірно?) При використанні доменного імені або глобального IP. Можливо, ви зможете поліпшити ситуацію, глянувши https://yunohost.org/dns_local_network ",
+ "diagnosis_http_hairpinning_issue": "Схоже, що у вашій локальній мережі не увімкнено шпилькування (hairpinning).",
+ "diagnosis_ports_forwarding_tip": "Щоб вирішити цю проблему, вам, швидше за все, потрібно налаштувати пересилання портів на вашому інтернет-маршрутизаторі, як описано в https://yunohost.org/isp_box_config",
+ "diagnosis_ports_needed_by": "Відкриття цього порту необхідне для функцій {category} (служба {service})",
+ "diagnosis_ports_ok": "Порт {port} доступний ззовні.",
+ "diagnosis_ports_partially_unreachable": "Порт {port} не доступний ззовні в IPv{failed}.",
+ "diagnosis_ports_unreachable": "Порт {port} недоступний ззовні.",
+ "diagnosis_ports_could_not_diagnose_details": "Помилка: {error}",
+ "diagnosis_ports_could_not_diagnose": "Не вдалося діагностувати досяжність портів ззовні в IPv{ipversion}.",
+ "diagnosis_description_regenconf": "Конфігурації системи",
+ "diagnosis_description_mail": "Е-пошта",
+ "diagnosis_description_ports": "Виявлення портів",
+ "diagnosis_description_systemresources": "Системні ресурси",
+ "diagnosis_description_services": "Перевірка стану служб",
+ "diagnosis_description_dnsrecords": "DNS-записи",
+ "diagnosis_description_ip": "Інтернет-з'єднання",
+ "diagnosis_description_basesystem": "Основна система",
+ "diagnosis_security_vulnerable_to_meltdown_details": "Щоб виправити це, вам слід оновити систему і перезавантажитися, щоб завантажити нове ядро Linux (або звернутися до вашого серверного провайдера, якщо це не спрацює). Докладніше див. на сайті https://meltdownattack.com/.",
+ "diagnosis_security_vulnerable_to_meltdown": "Схоже, що ви вразливі до критичної вразливості безпеки Meltdown",
+ "diagnosis_rootfstotalspace_critical": "Коренева файлова система має тільки {space}, що дуже тривожно! Скоріше за все, дисковий простір закінчиться дуже скоро! Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.",
+ "diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {space}. Можливо це нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися... Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.",
+ "diagnosis_regenconf_manually_modified_details": "Можливо це нормально, якщо ви знаєте, що робите! YunoHost перестане оновлювати цей файл автоматично... Але врахуйте, що оновлення YunoHost можуть містити важливі рекомендовані зміни. Якщо ви хочете, ви можете перевірити відмінності за допомогою команди yunohost tools regen-conf {category} --dry-run --with-diff і примусово повернути рекомендовану конфігурацію за допомогою команди yunohost tools regen-conf {category} --force ",
+ "diagnosis_regenconf_manually_modified": "Конфігураційний файл {file}
, схоже, було змінено вручну.",
+ "diagnosis_regenconf_allgood": "Усі конфігураційні файли відповідають рекомендованій конфігурації!",
+ "diagnosis_mail_queue_too_big": "Занадто багато відкладених листів у поштовій черзі (листів: {nb_pending})",
+ "diagnosis_mail_queue_unavailable_details": "Помилка: {error}",
+ "diagnosis_mail_queue_unavailable": "Неможливо дізнатися кількість очікувальних листів у черзі",
+ "diagnosis_mail_queue_ok": "Відкладених електронних листів у поштових чергах: {nb_pending}",
+ "diagnosis_mail_blacklist_website": "Після визначення причини, з якої ви потрапили в чорний список, і її усунення, ви можете попросити видалити ваш IP або домен на {blacklist_website}",
+ "diagnosis_mail_blacklist_reason": "Причина внесення в чорний список: {reason}",
+ "diagnosis_mail_blacklist_listed_by": "Ваш IP або домен {item}
знаходиться в чорному списку {blacklist_name}",
+ "diagnosis_mail_blacklist_ok": "IP-адреси і домени, які використовуються цим сервером, не внесені в чорний список",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Поточний зворотний DNS:{rdns_domain}
Очікуване значення: {ehlo_domain}
",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Зворотний DNS неправильно налаштований в IPv{ipversion}. Деякі електронні листи можуть бути не доставлені або можуть бути відзначені як спам.",
+ "diagnosis_mail_fcrdns_nok_alternatives_6": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати вимкнути використання IPv6 при надсиланні листів, виконавши команду yunohost settings set smtp.allow_ipv6 -v off . Примітка: останнє рішення означає, що ви не зможете надсилати або отримувати електронні листи з нечисленних серверів, що використовують тільки IPv6.",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана...). Якщо ви відчуваєте проблеми через це, розгляньте наступні рішення:
- Деякі провайдери надають альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу подібних обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Або можна переключитися на іншого провайдера",
+ "diagnosis_mail_fcrdns_nok_details": "Спочатку спробуйте налаштувати зворотний DNS з {ehlo_domain}
в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм запит у підтримку для цього).",
+ "diagnosis_mail_fcrdns_dns_missing": "У IPv{ipversion} не визначений зворотний DNS. Деякі листи можуть не доставлятися або позначатися як спам.",
+ "diagnosis_mail_fcrdns_ok": "Ваш зворотний DNS налаштовано правильно!",
+ "diagnosis_mail_ehlo_could_not_diagnose_details": "Помилка: {error}",
+ "diagnosis_mail_ehlo_could_not_diagnose": "Не вдалося діагностувати, чи доступний поштовий сервер postfix ззовні в IPv{ipversion}.",
+ "diagnosis_mail_ehlo_wrong_details": "EHLO, отриманий віддаленим діагностичним центром в IPv{ipversion}, відрізняється від домену вашого сервера.
Отриманий EHLO: {wrong_ehlo}
Очікуваний: {right_ehlo}
< br>Найпоширенішою причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер. Крім того, переконайтеся, що в роботу сервера не втручається фаєрвол або зворотний проксі-сервер.",
+ "diagnosis_mail_ehlo_wrong": "Інший поштовий SMTP-сервер відповідає на IPv{ipversion}. Ваш сервер, ймовірно, не зможе отримувати електронні листи.",
+ "diagnosis_mail_ehlo_bad_answer_details": "Це може бути викликано тим, що замість вашого сервера відповідає інша машина.",
+ "diagnosis_mail_ehlo_bad_answer": "Не-SMTP служба відповіла на порту 25 на IPv{ipversion}",
+ "diagnosis_mail_ehlo_unreachable_details": "Не вдалося відкрити з'єднання за портом 25 з вашим сервером на IPv{ipversion}. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер.
2. Ви також повинні переконатися, що служба postfix запущена.
3. На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.",
+ "diagnosis_mail_ehlo_unreachable": "Поштовий сервер SMTP недоступний ззовні по IPv{ipversion}. Він не зможе отримувати листи електронної пошти.",
+ "diagnosis_mail_ehlo_ok": "Поштовий сервер SMTP доступний ззовні і тому може отримувати електронні листи!",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Деякі провайдери не дозволять вам розблокувати вихідний порт 25, тому що вони не піклуються про мережевий нейтралітет (Net Neutrality).
- Деякі з них пропонують альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу такого роду обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Ви також можете розглянути можливість переходу на більш дружнього до мережевого нейтралітету провайдера",
+ "diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).",
+ "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.",
+ "app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок",
+ "yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - додавання першого користувача через розділ 'Користувачі' вебадміністрації (або 'yunohost user create ' в командному рядку);\n - діагностика можливих проблем через розділ 'Діагностика' вебадміністрації (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.",
+ "yunohost_not_installed": "YunoHost установлений неправильно. Будь ласка, запустіть 'yunohost tools postinstall'",
+ "yunohost_installing": "Установлення YunoHost...",
+ "yunohost_configured": "YunoHost вже налаштовано",
+ "yunohost_already_installed": "YunoHost вже встановлено",
+ "user_updated": "Відомості про користувача змінено",
+ "user_update_failed": "Не вдалося оновити користувача {user}: {error}",
+ "user_unknown": "Невідомий користувач: {user}",
+ "user_home_creation_failed": "Не вдалося створити каталог домівки для користувача",
+ "user_deletion_failed": "Не вдалося видалити користувача {user}: {error}",
+ "user_deleted": "Користувача видалено",
+ "user_creation_failed": "Не вдалося створити користувача {user}: {error}",
+ "user_created": "Користувача створено",
+ "user_already_exists": "Користувач '{user}' вже існує",
+ "upnp_port_open_failed": "Не вдалося відкрити порт через UPnP",
+ "upnp_enabled": "UPnP увімкнено",
+ "upnp_disabled": "UPnP вимкнено",
+ "upnp_dev_not_found": "UPnP-пристрій не знайдено",
+ "upgrading_packages": "Оновлення пакетів...",
+ "upgrade_complete": "Оновлення завершено",
+ "updating_apt_cache": "Завантаження доступних оновлень для системних пакетів...",
+ "update_apt_cache_warning": "Щось пішло не так при оновленні кеша APT (менеджера пакунків Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}",
+ "update_apt_cache_failed": "Неможливо оновити кеш APT (менеджер пакетів Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}",
+ "unrestore_app": "{app} не буде оновлено",
+ "unlimit": "Квоти немає",
+ "unknown_main_domain_path": "Невідомий домен або шлях для '{app}'. Вам необхідно вказати домен і шлях, щоб мати можливість вказати URL для дозволу.",
+ "unexpected_error": "Щось пішло не так: {error}",
+ "unbackup_app": "{app} НЕ буде збережено",
+ "tools_upgrade_special_packages_completed": "Оновлення пакета YunoHost завершено.\nНатисніть [Enter] для повернення до командного рядка",
+ "tools_upgrade_special_packages_explanation": "Спеціальне оновлення триватиме у тлі. Будь ласка, не запускайте ніяких інших дій на вашому сервері протягом наступних ~ 10 хвилин (в залежності від швидкості обладнання). Після цього вам, можливо, доведеться заново увійти в вебадміністрації. Журнал оновлення буде доступний в Засоби → Журнал (в вебадміністрації) або за допомогою 'yunohost log list' (з командного рядка).",
+ "tools_upgrade_special_packages": "Тепер оновлюємо 'спеціальні' (пов'язані з yunohost) пакети…",
+ "tools_upgrade_regular_packages_failed": "Не вдалося оновити пакети: {packages_list}",
+ "tools_upgrade_regular_packages": "Тепер оновлюємо 'звичайні' (не пов'язані з yunohost) пакети…",
+ "tools_upgrade_cant_unhold_critical_packages": "Не вдалося розтримати критичні пакети…",
+ "tools_upgrade_cant_hold_critical_packages": "Не вдалося утримати критичні пакети…",
+ "tools_upgrade_cant_both": "Неможливо оновити систему і застосунки одночасно",
+ "tools_upgrade_at_least_one": "Будь ласка, вкажіть 'apps', або 'system'",
+ "this_action_broke_dpkg": "Ця дія порушила dpkg/APT (системні менеджери пакетів)... Ви можете спробувати вирішити цю проблему, під'єднавшись по SSH і запустивши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`.",
+ "system_username_exists": "Ім'я користувача вже існує в списку користувачів системи",
+ "system_upgraded": "Систему оновлено",
+ "ssowat_conf_updated": "Конфігурацію SSOwat оновлено",
+ "ssowat_conf_generated": "Конфігурацію SSOwat перестворено",
+ "show_tile_cant_be_enabled_for_regex": "Ви не можете увімкнути 'show_tile' прямо зараз, тому що URL для дозволу '{permission}' являє собою регулярний вираз",
+ "show_tile_cant_be_enabled_for_url_not_defined": "Ви не можете увімкнути 'show_tile' прямо зараз, тому що спочатку ви повинні визначити URL для дозволу '{permission}'",
+ "service_unknown": "Невідома служба '{service}'",
+ "service_stopped": "Службу '{service}' зупинено",
+ "service_stop_failed": "Неможливо зупинити службу '{service}' \n\nНедавні журнали служби: {logs}",
+ "service_started": "Службу '{service}' запущено",
+ "service_start_failed": "Не вдалося запустити службу '{service}' \n\nНедавні журнали служби: {logs}",
+ "diagnosis_mail_outgoing_port_25_ok": "Поштовий сервер SMTP може відправляти електронні листи (вихідний порт 25 не заблоковано).",
+ "diagnosis_swap_tip": "Будь ласка, будьте обережні і знайте, що якщо сервер розміщує обсяг підкачки на SD-карті або SSD-накопичувачі, це може різко скоротити строк служби пристрою`.",
+ "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": "У сховищі {mountpoint}
(на пристрої {device}
) залишилося {free} ({free_percent}%) вільного місця (з {total})!",
+ "diagnosis_diskusage_low": "Сховище {mountpoint}
(на пристрої {device}
) має тільки {free} ({free_percent}%) вільного місця (з {total}). Будьте уважні.",
+ "diagnosis_diskusage_verylow": "Сховище {mountpoint}
(на пристрої {device}
) має тільки {free} ({free_percent}%) вільного місця (з {total}). Вам дійсно варто подумати про очищення простору!",
+ "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу, а якщо це не допоможе, подивіться журнали служби в вебадміністрації (з командного рядка це можна зробити за допомогою yunohost service restart {service} і yunohost service log {service} ).",
+ "diagnosis_services_bad_status": "Служба {service} у стані {status} :(",
+ "diagnosis_services_conf_broken": "Для служби {service} порушена конфігурація!",
+ "diagnosis_services_running": "Службу {service} запущено!",
+ "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_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.",
+ "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost dyndns update --force .",
+ "diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті https://yunohost.org/dns_config.",
+ "diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації:
Тип: {type}
Назва: {name}
Поточне значення: {current}
Очікуване значення: {value}
",
+ "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.
Тип: {type}
Назва: {name}
Значення: {value}
",
+ "diagnosis_dns_bad_conf": "Деякі DNS-записи відсутні або неправильні для домену {domain} (категорія {category})",
+ "diagnosis_dns_good_conf": "DNS-записи правильно налаштовані для домену {domain} (категорія {category})",
+ "diagnosis_ip_weird_resolvconf_details": "Файл /etc/resolv.conf
повинен бути символічним посиланням на /etc/resolvconf/run/resolv.conf
, що вказує на 127.0.0.1
(dnsmasq). Якщо ви хочете вручну налаштувати DNS вирішувачі (resolvers), відредагуйте /etc/resolv.dnsmasq.conf
.",
+ "diagnosis_ip_weird_resolvconf": "Роздільність DNS, схоже, працює, але схоже, що ви використовуєте користувацьку /etc/resolv.conf
.",
+ "diagnosis_ip_broken_resolvconf": "Схоже, що роздільність доменних імен на вашому сервері порушено, що пов'язано з тим, що /etc/resolv.conf
не вказує на 127.0.0.1
.",
+ "diagnosis_ip_broken_dnsresolution": "Роздільність доменних імен, схоже, з якоїсь причини не працює... Фаєрвол блокує DNS-запити?",
+ "diagnosis_ip_dnsresolution_working": "Роздільність доменних імен працює!",
+ "diagnosis_ip_not_connected_at_all": "Здається, сервер взагалі не під'єднаний до Інтернету!?",
+ "diagnosis_ip_local": "Локальний IP: {local}
",
+ "diagnosis_ip_global": "Глобальний IP: {global}
",
+ "diagnosis_ip_no_ipv6_tip": "Наявність робочого IPv6 не є обов'язковим для роботи вашого сервера, але це краще для здоров'я Інтернету в цілому. IPv6 зазвичай автоматично налаштовується системою або вашим провайдером, якщо він доступний. В іншому випадку вам, можливо, доведеться налаштувати деякі речі вручну, як пояснюється в документації тут: https://yunohost.org/#/ipv6. Якщо ви не можете увімкнути 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": "Неможливо запустити діагностику для {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_to_fix} ",
+ "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": "{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. Деякі функції, такі як єдина реєстрація та резервне копіювання/відновлення, можуть бути недоступні. Все одно встановити? [{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. Будь ласка, перевірте категорію 'Мережа' в діагностиці для отримання додаткових даних. (Якщо ви знаєте, що робите, використовуйте '--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} (файл: {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-конфігурації відсутній відповідний фрагмент коду... Будь ласка, переконайтеся, що конфігурація 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": "Запуск гачків (hook) резервного копіювання...",
+ "backup_permission": "Дозвіл на резервне копіювання для {app}",
+ "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_no_uncompress_archive_dir": "Немає такого каталогу нестислого архіву",
+ "backup_mount_archive_for_restore": "Підготовлення архіву для відновлення...",
+ "backup_method_tar_finished": "Створено архів резервного копіювання TAR",
+ "backup_method_custom_finished": "Користувацький спосіб резервного копіювання '{method}' завершено",
+ "backup_method_copy_finished": "Резервне копіювання завершено",
+ "backup_hook_unknown": "Гачок (hook) резервного копіювання '{hook}' невідомий",
+ "backup_deleted": "Резервна копія видалена",
+ "backup_delete_error": "Не вдалося видалити '{path}'",
+ "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": "Архів буде містити близько {size} даних.",
+ "backup_created": "Резервна копія створена",
+ "backup_couldnt_bind": "Не вдалося зв'язати {src} з {dest}.",
+ "backup_copying_to_organize_the_archive": "Копіювання {size} МБ для організації архіву",
+ "backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання",
+ "backup_cant_mount_uncompress_archive": "Не вдалося змонтувати нестислий архів як захищений від запису",
+ "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_app_not_found": "Не вдалося знайти {app} в архіві резервного копіювання",
+ "backup_applying_method_tar": "Створення резервного TAR-архіву...",
+ "backup_applying_method_custom": "Виклик користувацького способу резервного копіювання '{method}'...",
+ "backup_applying_method_copy": "Копіювання всіх файлів у резервну копію...",
+ "backup_app_failed": "Не вдалося створити резервну копію {app}",
+ "backup_actually_backuping": "Створення резервного архіву з зібраних файлів...",
+ "backup_abstract_method": "Цей спосіб резервного копіювання ще не реалізований",
+ "ask_password": "Пароль",
+ "ask_new_path": "Новий шлях",
+ "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": "Сталася помилка в скрипті оновлення застосунку",
+ "app_upgrade_failed": "Не вдалося оновити {app}: {error}",
+ "app_upgrade_app_name": "Зараз оновлюємо {app}...",
+ "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_install": "Установлення {app}...",
+ "app_sources_fetch_failed": "Не вдалося отримати джерельні файли, URL-адреса правильна?",
+ "app_restore_script_failed": "Сталася помилка всередині скрипта відновлення застосунку",
+ "app_restore_failed": "Не вдалося відновити {app}: {error}",
+ "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": "Виберіть пароль адміністрації для цього застосунку",
+ "diagnosis_description_apps": "Застосунки",
+ "user_import_success": "Користувачів успішно імпортовано",
+ "user_import_nothing_to_do": "Не потрібно імпортувати жодного користувача",
+ "user_import_failed": "Операція імпорту користувачів цілковито не вдалася",
+ "user_import_partial_failed": "Операція імпорту користувачів частково не вдалася",
+ "user_import_missing_columns": "Відсутні такі стовпці: {columns}",
+ "user_import_bad_file": "Ваш файл CSV неправильно відформатовано, він буде знехтуваний, щоб уникнути потенційної втрати даних",
+ "user_import_bad_line": "Неправильний рядок {line}: {details}",
+ "invalid_password": "Недійсний пароль",
+ "log_user_import": "Імпорт користувачів",
+ "ldap_server_is_down_restart_it": "Службу LDAP вимкнено, спробуйте перезапустити її...",
+ "ldap_server_down": "Не вдається під'єднатися до сервера LDAP",
+ "global_settings_setting_security_experimental_enabled": "Увімкнути експериментальні функції безпеки (не вмикайте це, якщо ви не знаєте, що робите!)",
+ "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надзастарілі практики упакування. Вам дійсно варто подумати про його оновлення.",
+ "diagnosis_apps_outdated_ynh_requirement": "Установлена версія цього застосунку вимагає лише Yunohost >= 2.x, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.",
+ "diagnosis_apps_bad_quality": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.",
+ "diagnosis_apps_broken": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.",
+ "diagnosis_apps_not_in_app_catalog": "Цей застосунок не міститься у каталозі застосунків YunoHost. Якщо він був у минулому і був видалений, вам слід подумати про видалення цього застосунку, оскільки він не отримає оновлення, і це може поставити під загрозу цілісність та безпеку вашої системи.",
+ "diagnosis_apps_issue": "Виявлено проблему із застосунком {app}",
+ "diagnosis_apps_allgood": "Усі встановлені застосунки дотримуються основних способів упакування",
+ "diagnosis_high_number_auth_failures": "Останнім часом сталася підозріло велика кількість помилок автентифікації. Ви можете переконатися, що fail2ban працює і правильно налаштований, або скористатися власним портом для SSH, як описано в https://yunohost.org/security.",
+ "global_settings_setting_security_nginx_redirect_to_https": "Типово переспрямовувати HTTP-запити до HTTP (НЕ ВИМИКАЙТЕ, якщо ви дійсно не знаєте, що робите!)",
+ "app_config_unable_to_apply": "Не вдалося застосувати значення панелі конфігурації.",
+ "app_config_unable_to_read": "Не вдалося розпізнати значення панелі конфігурації.",
+ "config_apply_failed": "Не вдалося застосувати нову конфігурацію: {error}",
+ "config_cant_set_value_on_section": "Ви не можете встановити одне значення на весь розділ конфігурації.",
+ "config_forbidden_keyword": "Ключове слово '{keyword}' зарезервовано, ви не можете створити або використовувати панель конфігурації з запитом із таким ID.",
+ "config_no_panel": "Панель конфігурації не знайдено.",
+ "config_unknown_filter_key": "Ключ фільтра '{filter_key}' недійсний.",
+ "config_validate_color": "Колір RGB має бути дійсним шістнадцятковим кольоровим кодом",
+ "config_validate_date": "Дата має бути дійсною, наприклад, у форматі РРРР-ММ-ДД",
+ "config_validate_email": "Е-пошта має бути дійсною",
+ "config_validate_time": "Час має бути дійсним, наприклад ГГ:ХХ",
+ "config_validate_url": "Вебадреса має бути дійсною",
+ "config_version_not_supported": "Версії конфігураційної панелі '{version}' не підтримуються.",
+ "danger": "Небезпека:",
+ "file_extension_not_accepted": "Файл '{path}' відхиляється, бо його розширення не входить в число прийнятих розширень: {accept}",
+ "invalid_number_min": "Має бути більшим за {min}",
+ "invalid_number_max": "Має бути меншим за {max}",
+ "log_app_config_set": "Застосувати конфігурацію до застосунку '{}'",
+ "service_not_reloading_because_conf_broken": "Неможливо перезавантажити/перезапустити службу '{name}', тому що її конфігурацію порушено: {errors}",
+ "app_argument_password_help_optional": "Введіть один пробіл, щоб очистити пароль",
+ "app_argument_password_help_keep": "Натисніть Enter, щоб зберегти поточне значення",
+ "domain_registrar_is_not_configured": "Реєстратор ще не конфігуровано для домену {domain}.",
+ "domain_dns_push_not_applicable": "Функція автоматичної конфігурації DNS не застосовується до домену {domain}. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://yunohost.org/dns_config.",
+ "domain_dns_registrar_not_supported": "YunoHost не зміг автоматично виявити реєстратора, який обробляє цей домен. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://yunohost.org/dns.",
+ "diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.",
+ "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.",
+ "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.",
+ "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')",
+ "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.",
+ "domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://yunohost.org/registar_api_{registrar}. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://yunohost.org/dns)",
+ "domain_dns_registrar_experimental": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!",
+ "domain_dns_push_success": "Записи DNS оновлено!",
+ "domain_dns_push_failed": "Оновлення записів DNS зазнало невдачі.",
+ "domain_dns_push_partial_failure": "DNS-записи частково оновлено: повідомлялося про деякі попередження/помилки.",
+ "domain_config_mail_in": "Вхідні електронні листи",
+ "domain_config_mail_out": "Вихідні електронні листи",
+ "domain_config_auth_token": "Токен автентифікації",
+ "domain_config_auth_entrypoint": "Точка входу API",
+ "domain_config_auth_consumer_key": "Ключ споживача",
+ "domain_dns_push_failed_to_authenticate": "Неможливо пройти автентифікацію на API реєстратора для домену '{domain}'. Ймовірно, облікові дані недійсні? (Помилка: {error})",
+ "domain_dns_push_failed_to_list": "Не вдалося скласти список поточних записів за допомогою API реєстратора: {error}",
+ "domain_dns_push_record_failed": "Не вдалося виконати дію {action} запису {type}/{name} : {error}",
+ "domain_config_features_disclaimer": "Поки що вмикання/вимикання функцій пошти або XMPP впливає тільки на рекомендовану та автоконфігурацію DNS, але не на конфігурацію системи!",
+ "domain_config_xmpp": "Миттєвий обмін повідомленнями (XMPP)",
+ "domain_config_auth_key": "Ключ автентифікації",
+ "domain_config_auth_secret": "Секрет автентифікації",
+ "domain_config_api_protocol": "API-протокол",
+ "domain_config_auth_application_key": "Ключ застосунку",
+ "domain_config_auth_application_secret": "Таємний ключ застосунку",
+ "log_domain_config_set": "Оновлення конфігурації для домену '{}'",
+ "log_domain_dns_push": "Передавання записів DNS для домену '{}'",
+ "other_available_options": "...і {n} інших доступних опцій, які не показано",
+ "domain_dns_pushing": "Передання записів DNS...",
+ "ldap_attribute_already_exists": "Атрибут LDAP '{attribute}' вже існує зі значенням '{value}'",
+ "domain_dns_push_already_up_to_date": "Записи вже оновлені, нічого не потрібно робити."
+}
diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json
index d11e570d0..9176ebab9 100644
--- a/locales/zh_Hans.json
+++ b/locales/zh_Hans.json
@@ -2,15 +2,628 @@
"password_too_simple_1": "密码长度至少为8个字符",
"backup_created": "备份已创建",
"app_start_remove": "正在删除{app}……",
- "admin_password_change_failed": "不能修改密码",
+ "admin_password_change_failed": "无法修改密码",
"admin_password_too_long": "请选择一个小于127个字符的密码",
- "app_upgrade_failed": "不能升级{app:s}:{error}",
+ "app_upgrade_failed": "不能升级{app}:{error}",
"app_id_invalid": "无效 app ID",
"app_unknown": "未知应用",
"admin_password_changed": "管理密码已更改",
"aborting": "正在放弃。",
- "admin_password": "管理密码",
+ "admin_password": "管理员密码",
"app_start_restore": "正在恢复{app}……",
- "action_invalid": "无效操作 '{action:s}'",
- "ask_lastname": "姓"
-}
+ "action_invalid": "无效操作 '{action}'",
+ "ask_lastname": "姓",
+ "diagnosis_everything_ok": "{category}一切看起来不错!",
+ "diagnosis_found_warnings": "找到{warnings}项,可能需要{category}进行改进。",
+ "diagnosis_found_errors_and_warnings": "发现与{category}相关的{errors}个重要问题(和{warnings}警告)!",
+ "diagnosis_found_errors": "发现与{category}相关的{errors}个重要问题!",
+ "diagnosis_ignored_issues": "(+ {nb_ignored} 个被忽略的问题)",
+ "diagnosis_cant_run_because_of_dep": "存在与{dep}相关的重要问题时,无法对{category}进行诊断。",
+ "diagnosis_cache_still_valid": "(高速缓存对于{category}诊断仍然有效。暂时不会对其进行重新诊断!)",
+ "diagnosis_failed_for_category": "诊断类别 '{category}'失败: {error}",
+ "diagnosis_display_tip": "要查看发现的问题,您可以转到Webadmin的“诊断”部分,或从命令行运行'yunohost diagnosis show --issues --human-readable'。",
+ "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": "{package} 版本: {version} ({repo})",
+ "diagnosis_basesystem_kernel": "服务器正在运行Linux kernel {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很好地集成。某些功能(例如单点登录和备份/还原)可能不可用, 仍要安装吗? [{answers}] ",
+ "certmanager_unable_to_parse_self_CA_name": "无法解析自签名授权的名称 (file: {file})",
+ "certmanager_self_ca_conf_file_not_found": "找不到用于自签名授权的配置文件(file: {file})",
+ "certmanager_no_cert_file": "无法读取域{domain}的证书文件(file: {file})",
+ "certmanager_hit_rate_limit": "最近已经为此域{domain}颁发了太多的证书。请稍后再试。有关更多详细信息,请参见https://letsencrypt.org/docs/rate-limits/",
+ "certmanager_warning_subdomain_dns_record": "子域'{subdomain}' 不能解析为与 '{domain}'相同的IP地址, 在修复此问题并重新生成证书之前,某些功能将不可用。",
+ "certmanager_domain_http_not_working": "域 {domain}似乎无法通过HTTP访问。请检查诊断中的“网络”类别以获取更多信息。(如果您知道自己在做什么,请使用“ --no-checks”关闭这些检查。)",
+ "certmanager_domain_dns_ip_differs_from_public_ip": "域'{domain}' 的DNS记录与此服务器的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_certificate_fetching_or_enabling_failed": "尝试将新证书用于 {domain}无效...",
+ "certmanager_cert_signing_failed": "无法签署新证书",
+ "certmanager_cert_install_success_selfsigned": "为域 '{domain}'安装了自签名证书",
+ "certmanager_cert_renew_success": "为域 '{domain}'续订“Let's Encrypt”证书",
+ "certmanager_cert_install_success": "为域'{domain}'安装“Let's Encrypt”证书",
+ "certmanager_cannot_read_cert": "尝试为域 {domain}(file: {file})打开当前证书时发生错误,原因: {reason}",
+ "certmanager_attempt_to_replace_valid_cert": "您正在尝试覆盖域{domain}的有效证书!(使用--force绕过)",
+ "certmanager_attempt_to_renew_valid_cert": "域'{domain}'的证书不会过期!(如果知道自己在做什么,则可以使用--force)",
+ "certmanager_attempt_to_renew_nonLE_cert": "“Let's Encrypt”未颁发域'{domain}'的证书,无法自动续订!",
+ "certmanager_acme_not_configured_for_domain": "目前无法针对{domain}运行ACME挑战,因为其nginx conf缺少相应的代码段...请使用“yunohost tools regen-conf nginx --dry-run --with-diff”确保您的nginx配置是最新的。",
+ "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_permission": "{app}的备份权限",
+ "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_no_uncompress_archive_dir": "没有这样的未压缩存档目录",
+ "backup_mount_archive_for_restore": "正在准备存档以进行恢复...",
+ "backup_method_tar_finished": "TAR备份存档已创建",
+ "backup_method_custom_finished": "自定义备份方法'{method}' 已完成",
+ "backup_method_copy_finished": "备份副本已完成",
+ "backup_hook_unknown": "备用挂钩'{hook}'未知",
+ "backup_deleted": "备份已删除",
+ "backup_delete_error": "无法删除'{path}'",
+ "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": "归档文件将包含约{size}个数据。",
+ "backup_couldnt_bind": "无法将 {src} 绑定到{dest}.",
+ "backup_copying_to_organize_the_archive": "复制{size} MB来整理档案",
+ "backup_cleaning_failed": "无法清理临时备份文件夹",
+ "backup_cant_mount_uncompress_archive": "无法将未压缩的归档文件挂载为写保护",
+ "backup_ask_for_copying_if_needed": "您是否要临时使用{size} MB进行备份?(由于无法使用更有效的方法准备某些文件,因此使用这种方式。)",
+ "backup_archive_writing_error": "无法将要备份的文件'{source}'(在归档文'{dest}'中命名)添加到压缩归档文件 '{archive}'s}”中",
+ "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": "调用自定义备份方法'{method}'...",
+ "backup_applying_method_copy": "正在将所有文件复制到备份...",
+ "backup_app_failed": "无法备份{app}",
+ "backup_actually_backuping": "根据收集的文件创建备份档案...",
+ "backup_abstract_method": "此备份方法尚未实现",
+ "ask_password": "密码",
+ "ask_new_path": "新路径",
+ "ask_new_domain": "新域名",
+ "ask_new_admin_password": "新的管理密码",
+ "ask_main_domain": "主域",
+ "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}upgraded",
+ "app_upgrade_some_app_failed": "某些应用无法升级",
+ "app_upgrade_script_failed": "应用升级脚本内部发生错误",
+ "app_upgrade_app_name": "现在升级{app} ...",
+ "app_upgrade_several_apps": "以下应用将被升级: {apps}",
+ "app_unsupported_remote_type": "应用程序使用的远程类型不受支持",
+ "app_start_backup": "正在收集要备份的文件,用于{app} ...",
+ "app_start_install": "{app}安装中...",
+ "app_sources_fetch_failed": "无法获取源文件,URL是否正确?",
+ "app_restore_script_failed": "应用还原脚本内部发生错误",
+ "app_restore_failed": "无法还原 {app}: {error}",
+ "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_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": "选择此应用的管理密码",
+ "additional_urls_already_removed": "权限'{permission}'的其他URL中已经删除了附加URL'{url}'",
+ "app_manifest_install_ask_path": "选择安装此应用的路径",
+ "app_manifest_install_ask_domain": "选择应安装此应用程序的域",
+ "app_manifest_invalid": "应用清单错误: {error}",
+ "app_location_unavailable": "该URL不可用,或与已安装的应用冲突:\n{apps}",
+ "app_label_deprecated": "不推荐使用此命令!请使用新命令 'yunohost user permission update'来管理应用标签。",
+ "app_make_default_location_already_used": "无法将'{app}' 设置为域上的默认应用,'{other_app}'已在使用'{domain}'",
+ "app_install_script_failed": "应用安装脚本内发生错误",
+ "app_install_failed": "无法安装 {app}: {error}",
+ "app_install_files_invalid": "这些文件无法安装",
+ "additional_urls_already_added": "附加URL '{url}' 已添加到权限'{permission}'的附加URL中",
+ "app_full_domain_unavailable": "抱歉,此应用必须安装在其自己的域中,但其他应用已安装在域“ {domain}”上。 您可以改用专用于此应用程序的子域。",
+ "app_extraction_failed": "无法解压缩安装文件",
+ "app_change_url_success": "{app} URL现在为 {domain}{path}",
+ "app_change_url_no_script": "应用程序'{app_name}'尚不支持URL修改. 也许您应该升级它。",
+ "app_change_url_identical_domains": "新旧domain / url_path是相同的('{domain}{path}'),无需执行任何操作。",
+ "app_argument_required": "参数'{name}'为必填项",
+ "app_argument_password_no_default": "解析密码参数'{name}'时出错:出于安全原因,密码参数不能具有默认值",
+ "app_argument_invalid": "为参数'{name}'选择一个有效值: {error}",
+ "app_argument_choice_invalid": "对参数'{name}'使用以下选项之一'{choices}'",
+ "app_already_up_to_date": "{app} 已经是最新的",
+ "app_already_installed": "{app}已安装",
+ "app_action_broke_system": "该操作似乎破坏了以下重要服务:{services}",
+ "app_action_cannot_be_ran_because_required_services_down": "这些必需的服务应该正在运行以执行以下操作:{services},尝试重新启动它们以继续操作(考虑调查为什么它们出现故障)。",
+ "already_up_to_date": "无事可做。一切都已经是最新的了。",
+ "postinstall_low_rootfsspace": "根文件系统的总空间小于10 GB,这非常令人担忧!您可能很快就会用完磁盘空间!建议根文件系统至少有16GB, 如果尽管出现此警告仍要安装YunoHost,请使用--force-diskspace重新运行postinstall",
+ "port_already_opened": "{ip_version}个连接的端口 {port} 已打开",
+ "port_already_closed": "{ip_version}个连接的端口 {port} 已关闭",
+ "permission_require_account": "权限{permission}只对有账户的用户有意义,因此不能对访客启用。",
+ "permission_protected": "权限{permission}是受保护的。你不能向/从这个权限添加或删除访问者组。",
+ "permission_updated": "权限 '{permission}' 已更新",
+ "permission_update_failed": "无法更新权限 '{permission}': {error}",
+ "permission_not_found": "找不到权限'{permission}'",
+ "permission_deletion_failed": "无法删除权限 '{permission}': {error}",
+ "permission_deleted": "权限'{permission}' 已删除",
+ "permission_cant_add_to_all_users": "权限{permission}不能添加到所有用户。",
+ "regenconf_file_copy_failed": "无法将新的配置文件'{new}' 复制到'{conf}'",
+ "regenconf_file_backed_up": "将配置文件 '{conf}' 备份到 '{backup}'",
+ "regenconf_failed": "无法重新生成类别的配置: {categories}",
+ "regenconf_dry_pending_applying": "正在检查将应用于类别 '{category}'的待定配置…",
+ "regenconf_would_be_updated": "配置已更新为类别 '{category}'",
+ "regenconf_updated": "配置已针对'{category}'进行了更新",
+ "regenconf_now_managed_by_yunohost": "现在,配置文件'{conf}'由YunoHost(类别{category})管理。",
+ "regenconf_file_updated": "配置文件'{conf}' 已更新",
+ "regenconf_file_removed": "配置文件 '{conf}'已删除",
+ "regenconf_file_remove_failed": "无法删除配置文件 '{conf}'",
+ "regenconf_file_manually_removed": "配置文件'{conf}' 已手动删除,因此不会创建",
+ "regenconf_file_manually_modified": "配置文件'{conf}' 已被手动修改,不会被更新",
+ "regenconf_need_to_explicitly_specify_ssh": "ssh配置已被手动修改,但是您需要使用--force明确指定类别“ ssh”才能实际应用更改。",
+ "restore_nothings_done": "什么都没有恢复",
+ "restore_may_be_not_enough_disk_space": "您的系统似乎没有足够的空间(可用空间: {free_space} B,所需空间: {needed_space} B,安全系数: {margin} B)",
+ "restore_hook_unavailable": "'{part}'的恢复脚本在您的系统上和归档文件中均不可用",
+ "restore_failed": "无法还原系统",
+ "restore_extracting": "正在从存档中提取所需文件…",
+ "restore_confirm_yunohost_installed": "您真的要还原已经安装的系统吗? [{answers}]",
+ "restore_complete": "恢复完成",
+ "restore_cleaning_failed": "无法清理临时还原目录",
+ "restore_backup_too_old": "无法还原此备份存档,因为它来自过旧的YunoHost版本。",
+ "restore_already_installed_apps": "以下应用已安装,因此无法还原: {apps}",
+ "restore_already_installed_app": "已安装ID为'{app}' 的应用",
+ "regex_with_only_domain": "您不能将正则表达式用于域,而只能用于路径",
+ "regex_incompatible_with_tile": "/!\\ 打包者!权限“ {permission}”的show_tile设置为“ true”,因此您不能将正则表达式URL定义为主URL",
+ "service_cmd_exec_failed": "无法执行命令'{command}'",
+ "service_already_stopped": "服务'{service}'已被停止",
+ "service_already_started": "服务'{service}' 已在运行",
+ "service_added": "服务 '{service}'已添加",
+ "service_add_failed": "无法添加服务 '{service}'",
+ "server_reboot_confirm": "服务器会立即重启,确定吗? [{answers}]",
+ "server_reboot": "服务器将重新启动",
+ "server_shutdown_confirm": "服务器会立即关闭,确定吗?[{answers}]",
+ "server_shutdown": "服务器将关闭",
+ "root_password_replaced_by_admin_password": "您的root密码已替换为您的管理员密码。",
+ "root_password_desynchronized": "管理员密码已更改,但是YunoHost无法将此密码传播到root密码!",
+ "restore_system_part_failed": "无法还原 '{part}'系统部分",
+ "restore_running_hooks": "运行修复挂钩…",
+ "restore_running_app_script": "正在还原应用'{app}'…",
+ "restore_removing_tmp_dir_failed": "无法删除旧的临时目录",
+ "service_description_yunohost-firewall": "管理打开和关闭服务的连接端口",
+ "service_description_yunohost-api": "管理YunoHost Web界面与系统之间的交互",
+ "service_description_ssh": "允许您通过终端(SSH协议)远程连接到服务器",
+ "service_description_slapd": "存储用户、域名和相关信息",
+ "service_description_rspamd": "过滤垃圾邮件和其他与电子邮件相关的功能",
+ "service_description_redis-server": "用于快速数据访问,任务队列和程序之间通信的专用数据库",
+ "service_description_postfix": "用于发送和接收电子邮件",
+ "service_description_php7.3-fpm": "使用NGINX运行用PHP编写的应用程序",
+ "service_description_nginx": "为你的服务器上托管的所有网站提供服务或访问",
+ "service_description_mysql": "存储应用程序数据(SQL数据库)",
+ "service_description_metronome": "管理XMPP即时消息传递帐户",
+ "service_description_fail2ban": "防止来自互联网的暴力攻击和其他类型的攻击",
+ "service_description_dovecot": "允许电子邮件客户端访问/获取电子邮件(通过IMAP和POP3)",
+ "service_description_dnsmasq": "处理域名解析(DNS)",
+ "service_started": "服务 '{service}' 已启动",
+ "service_start_failed": "无法启动服务 '{service}'\n\n最近的服务日志:{logs}",
+ "service_reloaded_or_restarted": "服务'{service}'已重新加载或重新启动",
+ "service_reload_or_restart_failed": "无法重新加载或重新启动服务'{service}'\n\n最近的服务日志:{logs}",
+ "service_restarted": "服务'{service}' 已重新启动",
+ "service_restart_failed": "无法重新启动服务 '{service}'\n\n最近的服务日志:{logs}",
+ "service_reloaded": "服务 '{service}' 已重新加载",
+ "service_reload_failed": "无法重新加载服务'{service}'\n\n最近的服务日志:{logs}",
+ "service_removed": "服务 '{service}' 已删除",
+ "service_remove_failed": "无法删除服务'{service}'",
+ "service_regen_conf_is_deprecated": "不建议使用'yunohost service regen-conf' ! 请改用'yunohost tools regen-conf'。",
+ "service_enabled": "现在,服务'{service}' 将在系统引导过程中自动启动。",
+ "service_enable_failed": "无法使服务 '{service}'在启动时自动启动。\n\n最近的服务日志:{logs}",
+ "service_disabled": "系统启动时,服务 '{service}' 将不再启动。",
+ "service_disable_failed": "服务'{service}'在启动时无法启动。\n\n最近的服务日志:{logs}",
+ "tools_upgrade_regular_packages": "现在正在升级 'regular' (与yunohost无关)的软件包…",
+ "tools_upgrade_cant_unhold_critical_packages": "无法解压关键软件包…",
+ "tools_upgrade_cant_hold_critical_packages": "无法保存重要软件包…",
+ "tools_upgrade_cant_both": "无法同时升级系统和应用程序",
+ "tools_upgrade_at_least_one": "请指定'apps', 或 'system'",
+ "this_action_broke_dpkg": "此操作破坏了dpkg / APT(系统软件包管理器)...您可以尝试通过SSH连接并运行`sudo apt install --fix-broken`和/或`sudo dpkg --configure -a`来解决此问题。",
+ "system_username_exists": "用户名已存在于系统用户列表中",
+ "system_upgraded": "系统升级",
+ "ssowat_conf_updated": "SSOwat配置已更新",
+ "ssowat_conf_generated": "SSOwat配置已重新生成",
+ "show_tile_cant_be_enabled_for_regex": "你不能启用'show_tile',因为权限'{permission}'的URL是一个重合词",
+ "show_tile_cant_be_enabled_for_url_not_defined": "您现在无法启用 'show_tile' ,因为您必须先为权限'{permission}'定义一个URL",
+ "service_unknown": "未知服务 '{service}'",
+ "service_stopped": "服务'{service}' 已停止",
+ "service_stop_failed": "无法停止服务'{service}'\n\n最近的服务日志:{logs}",
+ "upnp_dev_not_found": "找不到UPnP设备",
+ "upgrading_packages": "升级程序包...",
+ "upgrade_complete": "升级完成",
+ "updating_apt_cache": "正在获取系统软件包的可用升级...",
+ "update_apt_cache_warning": "更新APT缓存(Debian的软件包管理器)时出了点问题。这是sources.list行的转储,这可能有助于确定有问题的行:\n{sourceslist}",
+ "update_apt_cache_failed": "无法更新APT的缓存(Debian的软件包管理器)。这是sources.list行的转储,这可能有助于确定有问题的行:\n{sourceslist}",
+ "unrestore_app": "{app} 将不会恢复",
+ "unlimit": "没有配额",
+ "unknown_main_domain_path": "'{app}'的域或路径未知。您需要指定一个域和一个路径,以便能够指定用于许可的URL。",
+ "unexpected_error": "出乎意料的错误: {error}",
+ "unbackup_app": "{app} 将不会保存",
+ "tools_upgrade_special_packages_completed": "YunoHost软件包升级完成。\n按[Enter]返回命令行",
+ "tools_upgrade_special_packages_explanation": "特殊升级将在后台继续。请在接下来的10分钟内(取决于硬件速度)在服务器上不要执行任何其他操作。此后,您可能必须重新登录Webadmin。升级日志将在“工具”→“日志”(在Webadmin中)或使用'yunohost log list'(从命令行)中可用。",
+ "tools_upgrade_special_packages": "现在正在升级'special'(与yunohost相关的)程序包…",
+ "tools_upgrade_regular_packages_failed": "无法升级软件包: {packages_list}",
+ "yunohost_installing": "正在安装YunoHost ...",
+ "yunohost_configured": "现在已配置YunoHost",
+ "yunohost_already_installed": "YunoHost已经安装",
+ "user_updated": "用户信息已更改",
+ "user_update_failed": "无法更新用户{user}: {error}",
+ "user_unknown": "未知用户: {user}",
+ "user_home_creation_failed": "无法为用户创建'home'文件夹",
+ "user_deletion_failed": "无法删除用户 {user}: {error}",
+ "user_deleted": "用户已删除",
+ "user_creation_failed": "无法创建用户 {user}: {error}",
+ "user_created": "用户创建",
+ "user_already_exists": "用户'{user}' 已存在",
+ "upnp_port_open_failed": "无法通过UPnP打开端口",
+ "upnp_enabled": "UPnP已启用",
+ "upnp_disabled": "UPnP已禁用",
+ "yunohost_not_installed": "YunoHost没有正确安装,请运行 'yunohost tools postinstall'",
+ "yunohost_postinstall_end_tip": "后期安装完成! 为了最终完成你的设置,请考虑:\n -通过webadmin的“用户”部分添加第一个用户(或在命令行中'yunohost user create ' );\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解YunoHost”部分: https://yunohost.org/admindoc.",
+ "operation_interrupted": "该操作是否被手动中断?",
+ "invalid_regex": "无效的正则表达式:'{regex}'",
+ "installation_complete": "安装完成",
+ "hook_name_unknown": "未知的钩子名称 '{name}'",
+ "hook_list_by_invalid": "此属性不能用于列出钩子",
+ "hook_json_return_error": "无法读取来自钩子 {path}的返回,错误: {msg}。原始内容: {raw_content}",
+ "hook_exec_not_terminated": "脚本未正确完成: {path}",
+ "hook_exec_failed": "无法运行脚本: {path}",
+ "group_user_not_in_group": "用户{user}不在组{group}中",
+ "group_user_already_in_group": "用户{user}已在组{group}中",
+ "group_update_failed": "无法更新群组'{group}': {error}",
+ "group_updated": "群组 '{group}' 已更新",
+ "group_unknown": "群组 '{group}' 未知",
+ "group_deletion_failed": "无法删除群组'{group}': {error}",
+ "group_deleted": "群组'{group}' 已删除",
+ "group_cannot_be_deleted": "无法手动删除组{group}。",
+ "group_cannot_edit_primary_group": "不能手动编辑 '{group}' 组。它是旨在仅包含一个特定用户的主要组。",
+ "group_cannot_edit_visitors": "组“访客”不能手动编辑。这是一个代表匿名访问者的特殊小组",
+ "group_cannot_edit_all_users": "组“ all_users”不能手动编辑。这是一个特殊的组,旨在包含所有在YunoHost中注册的用户",
+ "group_creation_failed": "无法创建组'{group}': {error}",
+ "group_created": "创建了 '{group}'组",
+ "group_already_exist_on_system_but_removing_it": "系统组中已经存在组{group},但是YunoHost会将其删除...",
+ "group_already_exist_on_system": "系统组中已经存在组{group}",
+ "group_already_exist": "群组{group}已经存在",
+ "good_practices_about_admin_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)。",
+ "global_settings_unknown_type": "意外的情况,设置{setting}似乎具有类型 {unknown_type} ,但是系统不支持该类型。",
+ "global_settings_setting_backup_compress_tar_archives": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量CPU。",
+ "global_settings_setting_smtp_relay_password": "SMTP中继主机密码",
+ "global_settings_setting_smtp_relay_user": "SMTP中继用户帐户",
+ "global_settings_setting_smtp_relay_port": "SMTP中继端口",
+ "global_settings_setting_smtp_allow_ipv6": "允许使用IPv6接收和发送邮件",
+ "global_settings_setting_ssowat_panel_overlay_enabled": "启用SSOwat面板覆盖",
+ "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "允许使用DSA主机密钥进行SSH守护程序配置(不建议使用)",
+ "global_settings_unknown_setting_from_settings_file": "设置中的未知密钥:'{setting_key}',将其丢弃并保存在/etc/yunohost/settings-unknown.json中",
+ "global_settings_setting_security_ssh_port": "SSH端口",
+ "global_settings_setting_security_postfix_compatibility": "Postfix服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)",
+ "global_settings_setting_security_ssh_compatibility": "SSH服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)",
+ "global_settings_setting_security_password_user_strength": "用户密码强度",
+ "global_settings_setting_security_password_admin_strength": "管理员密码强度",
+ "global_settings_setting_security_nginx_compatibility": "Web服务器NGINX的兼容性与安全性的权衡,影响密码(以及其他与安全性有关的方面)",
+ "global_settings_setting_pop3_enabled": "为邮件服务器启用POP3协议",
+ "global_settings_reset_success": "以前的设置现在已经备份到{path}",
+ "global_settings_key_doesnt_exists": "全局设置中不存在键'{settings_key}',您可以通过运行 'yunohost settings list'来查看所有可用键",
+ "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": "文件{path} 不存在。",
+ "field_invalid": "无效的字段'{}'",
+ "experimental_feature": "警告:此功能是实验性的,不稳定,请不要使用它,除非您知道自己在做什么。",
+ "extracting": "提取中...",
+ "dyndns_unavailable": "域'{domain}' 不可用。",
+ "dyndns_domain_not_provided": "DynDNS提供者 {provider} 无法提供域 {domain}。",
+ "dyndns_registration_failed": "无法注册DynDNS域: {error}",
+ "dyndns_registered": "DynDNS域已注册",
+ "dyndns_provider_unreachable": "无法联系DynDNS提供者 {provider}: 您的YunoHost未正确连接到Internet或dynette服务器已关闭。",
+ "dyndns_no_domain_registered": "没有在DynDNS中注册的域",
+ "dyndns_key_not_found": "找不到该域的DNS密钥",
+ "dyndns_key_generating": "正在生成DNS密钥...可能需要一段时间。",
+ "dyndns_ip_updated": "在DynDNS上更新了您的IP",
+ "dyndns_ip_update_failed": "无法将IP地址更新到DynDNS",
+ "dyndns_could_not_check_available": "无法检查{provider}上是否可用 {domain}。",
+ "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": "下载中…",
+ "done": "完成",
+ "domains_available": "可用域:",
+ "domain_name_unknown": "域'{domain}'未知",
+ "domain_uninstall_app_first": "这些应用程序仍安装在您的域中:\n{apps}\n\n请先使用 'yunohost app remove the_app_id' 将其卸载,或使用 'yunohost app change-url the_app_id'将其移至另一个域,然后再继续删除域",
+ "domain_remove_confirm_apps_removal": "删除该域将删除这些应用程序:\n{apps}\n\n您确定要这样做吗? [{answers}]",
+ "domain_hostname_failed": "无法设置新的主机名。稍后可能会引起问题(可能没问题)。",
+ "domain_exists": "该域已存在",
+ "domain_dyndns_root_unknown": "未知的DynDNS根域",
+ "domain_dyndns_already_subscribed": "您已经订阅了DynDNS域",
+ "domain_dns_conf_is_just_a_recommendation": "本页向你展示了*推荐的*配置。它并*不*为你配置DNS。你有责任根据该建议在你的DNS注册商处配置你的DNS区域。",
+ "domain_deletion_failed": "无法删除域 {domain}: {error}",
+ "domain_deleted": "域已删除",
+ "domain_creation_failed": "无法创建域 {domain}: {error}",
+ "domain_created": "域已创建",
+ "domain_cert_gen_failed": "无法生成证书",
+ "diagnosis_sshd_config_inconsistent": "看起来SSH端口是在/etc/ssh/sshd_config中手动修改, 从YunoHost 4.2开始,可以使用新的全局设置“ security.ssh.port”来避免手动编辑配置。",
+ "diagnosis_sshd_config_insecure": "SSH配置似乎已被手动修改,并且是不安全的,因为它不包含“ AllowGroups”或“ AllowUsers”指令以限制对授权用户的访问。",
+ "diagnosis_processes_killed_by_oom_reaper": "该系统最近杀死了某些进程,因为内存不足。这通常是系统内存不足或进程占用大量内存的征兆。 杀死进程的摘要:\n{kills_summary}",
+ "diagnosis_never_ran_yet": "看来这台服务器是最近安装的,还没有诊断报告可以显示。您应该首先从Web管理员运行完整的诊断,或者从命令行使用'yunohost diagnosis run' 。",
+ "diagnosis_unknown_categories": "以下类别是未知的: {categories}",
+ "diagnosis_http_nginx_conf_not_up_to_date_details": "要解决这种情况,请使用yunohost tools regen-conf nginx --dry-run --with-diff ,如果还可以,请使用yunohost tools regen-conf nginx --force 应用更改。",
+ "diagnosis_http_nginx_conf_not_up_to_date": "该域的nginx配置似乎已被手动修改,并阻止YunoHost诊断它是否可以在HTTP上访问。",
+ "diagnosis_http_partially_unreachable": "尽管域{domain}可以在 IPv{failed}中工作,但它似乎无法通过HTTP从外部网络通过HTTP到达IPv{passed}。",
+ "diagnosis_mail_outgoing_port_25_blocked_details": "您应该首先尝试在Internet路由器界面或主机提供商界面中取消阻止传出端口25。(某些托管服务提供商可能会要求您为此发送支持请求)。",
+ "diagnosis_mail_outgoing_port_25_blocked": "由于传出端口25在IPv{ipversion}中被阻止,因此SMTP邮件服务器无法向其他服务器发送电子邮件。",
+ "diagnosis_mail_outgoing_port_25_ok": "SMTP邮件服务器能够发送电子邮件(未阻止出站端口25)。",
+ "diagnosis_swap_tip": "请注意,如果服务器在SD卡或SSD存储器上托管交换,则可能会大大缩短设备的预期寿命。",
+ "diagnosis_swap_ok": "系统有{total}个交换!",
+ "diagnosis_swap_notsomuch": "系统只有{total}个交换。您应该考虑至少使用{recommended},以避免系统内存不足的情况。",
+ "diagnosis_swap_none": "系统根本没有交换分区。您应该考虑至少添加{recommended}交换,以避免系统内存不足的情况。",
+ "diagnosis_http_unreachable": "网域{domain}从本地网络外通过HTTP无法访问。",
+ "diagnosis_http_connection_error": "连接错误:无法连接到请求的域,很可能无法访问。",
+ "diagnosis_http_ok": "域{domain}可以通过HTTP从本地网络外部访问。",
+ "diagnosis_http_could_not_diagnose_details": "错误: {error}",
+ "diagnosis_http_could_not_diagnose": "无法诊断域是否可以从IPv{ipversion}中从外部访问。",
+ "diagnosis_http_hairpinning_issue_details": "这可能是由于您的ISP 光猫/路由器。因此,使用域名或全局IP时,来自本地网络外部的人员将能够按预期访问您的服务器,但无法访问来自本地网络内部的人员(可能与您一样)。您可以通过查看 https://yunohost.org/dns_local_network 来改善这种情况",
+ "diagnosis_http_hairpinning_issue": "您的本地网络似乎没有启用NAT回环功能。",
+ "diagnosis_ports_forwarding_tip": "要解决此问题,您很可能需要按照 https://yunohost.org/isp_box_config 中的说明,在Internet路由器上配置端口转发",
+ "diagnosis_ports_needed_by": "{category}功能(服务{service})需要公开此端口",
+ "diagnosis_ports_ok": "可以从外部访问端口{port}。",
+ "diagnosis_ports_partially_unreachable": "无法从外部通过IPv{failed}访问端口{port}。",
+ "diagnosis_ports_unreachable": "无法从外部访问端口{port}。",
+ "diagnosis_ports_could_not_diagnose_details": "错误: {error}",
+ "diagnosis_ports_could_not_diagnose": "无法诊断端口在IPv{ipversion}中是否可以从外部访问。",
+ "diagnosis_description_regenconf": "系统配置",
+ "diagnosis_description_mail": "电子邮件",
+ "diagnosis_description_web": "网页",
+ "diagnosis_description_ports": "开放端口",
+ "diagnosis_description_systemresources": "系统资源",
+ "diagnosis_description_services": "服务状态检查",
+ "diagnosis_description_dnsrecords": "DNS记录",
+ "diagnosis_description_ip": "互联网连接",
+ "diagnosis_description_basesystem": "基本系统",
+ "diagnosis_security_vulnerable_to_meltdown_details": "要解决此问题,您应该升级系统并重新启动以加载新的Linux内核(如果无法使用,请与您的服务器提供商联系)。有关更多信息,请参见https://meltdownattack.com/。",
+ "diagnosis_security_vulnerable_to_meltdown": "你似乎容易受到Meltdown关键安全漏洞的影响",
+ "diagnosis_regenconf_manually_modified": "配置文件 {file}
似乎已被手动修改。",
+ "diagnosis_regenconf_allgood": "所有配置文件均符合建议的配置!",
+ "diagnosis_mail_queue_too_big": "邮件队列中的待处理电子邮件过多({nb_pending} emails)",
+ "diagnosis_mail_queue_unavailable_details": "错误: {error}",
+ "diagnosis_mail_queue_unavailable": "无法查询队列中待处理电子邮件的数量",
+ "diagnosis_mail_queue_ok": "邮件队列中有{nb_pending} 个待处理的电子邮件",
+ "diagnosis_mail_blacklist_website": "确定列出的原因并加以修复后,请随时在{blacklist_website}上要求删除您的IP或域名",
+ "diagnosis_mail_blacklist_reason": "黑名单的原因是: {reason}",
+ "diagnosis_mail_blacklist_listed_by": "您的IP或域{item}
已在{blacklist_name}上列入黑名单",
+ "diagnosis_mail_blacklist_ok": "该服务器使用的IP和域似乎未列入黑名单",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "当前反向DNS值为: {rdns_domain}
期待值:{ehlo_domain}
",
+ "diagnosis_mail_fcrdns_different_from_ehlo_domain": "反向DNS未在 IPv{ipversion}中正确配置。某些电子邮件可能无法传递或可能被标记为垃圾邮件。",
+ "diagnosis_mail_fcrdns_nok_details": "您应该首先尝试在Internet路由器界面或托管服务提供商界面中使用{ehlo_domain}
配置反向DNS。(某些托管服务提供商可能会要求您为此发送支持票)。",
+ "diagnosis_mail_fcrdns_dns_missing": "IPv{ipversion}中未定义反向DNS。某些电子邮件可能无法传递或可能被标记为垃圾邮件。",
+ "diagnosis_mail_fcrdns_ok": "您的反向DNS已正确配置!",
+ "diagnosis_mail_ehlo_could_not_diagnose_details": "错误: {error}",
+ "diagnosis_mail_ehlo_could_not_diagnose": "无法诊断Postfix邮件服务器是否可以从IPv{ipversion}中从外部访问。",
+ "diagnosis_mail_ehlo_wrong": "不同的SMTP邮件服务器在IPv{ipversion}上进行应答。您的服务器可能无法接收电子邮件。",
+ "diagnosis_mail_ehlo_bad_answer_details": "这可能是由于其他计算机而不是您的服务器在应答。",
+ "diagnosis_mail_ehlo_bad_answer": "一个非SMTP服务在IPv{ipversion}的25端口应答",
+ "diagnosis_mail_ehlo_unreachable": "SMTP邮件服务器在IPv{ipversion}上无法从外部访问。它将无法接收电子邮件。",
+ "diagnosis_mail_ehlo_ok": "SMTP邮件服务器可以从外部访问,因此可以接收电子邮件!",
+ "diagnosis_services_bad_status": "服务{service}为 {status} :(",
+ "diagnosis_services_conf_broken": "服务{service}的配置已损坏!",
+ "diagnosis_services_running": "服务{service}正在运行!",
+ "diagnosis_domain_expires_in": "{domain}在{days}天后到期。",
+ "diagnosis_domain_expiration_error": "有些域很快就会过期!",
+ "diagnosis_domain_expiration_warning": "一些域即将过期!",
+ "diagnosis_domain_expiration_success": "您的域已注册,并且不会很快过期。",
+ "diagnosis_domain_expiration_not_found_details": "域{domain}的WHOIS信息似乎不包含有关到期日期的信息?",
+ "diagnosis_domain_not_found_details": "域{domain}在WHOIS数据库中不存在或已过期!",
+ "diagnosis_domain_expiration_not_found": "无法检查某些域的到期日期",
+ "diagnosis_dns_missing_record": "根据建议的DNS配置,您应该添加带有以下信息的DNS记录。
类型:{type}
名称:{name}
值:{value}
",
+ "diagnosis_dns_bad_conf": "域{domain}(类别{category})的某些DNS记录丢失或不正确",
+ "diagnosis_dns_good_conf": "已为域{domain}(类别{category})正确配置了DNS记录",
+ "diagnosis_ip_weird_resolvconf_details": "文件 /etc/resolv.conf
应该是指向 /etc/resolvconf/run/resolv.conf
本身的符号链接,指向 127.0.0.1
(dnsmasq)。如果要手动配置DNS解析器,请编辑 /etc/resolv.dnsmasq.conf
。",
+ "diagnosis_ip_weird_resolvconf": "DNS解析似乎可以正常工作,但是您似乎正在使用自定义的 /etc/resolv.conf
。",
+ "diagnosis_ip_broken_resolvconf": "域名解析在您的服务器上似乎已损坏,这似乎与 /etc/resolv.conf
有关,但未指向 127.0.0.1
。",
+ "diagnosis_ip_broken_dnsresolution": "域名解析似乎由于某种原因而被破坏...防火墙是否阻止了DNS请求?",
+ "diagnosis_ip_dnsresolution_working": "域名解析正常!",
+ "diagnosis_ip_not_connected_at_all": "服务器似乎根本没有连接到Internet?",
+ "diagnosis_ip_local": "本地IP:{local}
",
+ "diagnosis_ip_global": "全局IP: {global}
",
+ "diagnosis_ip_no_ipv6_tip": "正常运行的IPv6并不是服务器正常运行所必需的,但是对于整个Internet的健康而言,则更好。通常,IPv6应该由系统或您的提供商自动配置(如果可用)。否则,您可能需要按照此处的文档中的说明手动配置一些内容: https://yunohost.org/#/ipv6。如果您无法启用IPv6或对您来说太过困难,也可以安全地忽略此警告。",
+ "diagnosis_ip_no_ipv6": "服务器没有可用的IPv6。",
+ "diagnosis_ip_connected_ipv6": "服务器通过IPv6连接到Internet!",
+ "diagnosis_ip_no_ipv4": "服务器没有可用的IPv4。",
+ "diagnosis_ip_connected_ipv4": "服务器通过IPv4连接到Internet!",
+ "diagnosis_no_cache": "尚无类别 '{category}'的诊断缓存",
+ "diagnosis_failed": "无法获取类别 '{category}'的诊断结果: {error}",
+ "diagnosis_package_installed_from_sury_details": "一些软件包被无意中从一个名为Sury的第三方仓库安装。YunoHost团队改进了处理这些软件包的策略,但预计一些安装了PHP7.3应用程序的设置在仍然使用Stretch的情况下还有一些不一致的地方。为了解决这种情况,你应该尝试运行以下命令:{cmd_to_fix} ",
+ "app_not_installed": "在已安装的应用列表中找不到 {app}:{all_apps}",
+ "app_already_installed_cant_change_url": "这个应用程序已经被安装。URL不能仅仅通过这个函数来改变。在`app changeurl`中检查是否可用。",
+ "restore_not_enough_disk_space": "没有足够的空间(空间: {free_space} B,需要的空间: {needed_space} B,安全系数: {margin} B)",
+ "regenconf_pending_applying": "正在为类别'{category}'应用挂起的配置..",
+ "regenconf_up_to_date": "类别'{category}'的配置已经是最新的",
+ "regenconf_file_kept_back": "配置文件'{conf}'预计将被regen-conf(类别{category})删除,但被保留了下来。",
+ "good_practices_about_user_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)",
+ "global_settings_setting_smtp_relay_host": "使用SMTP中继主机来代替这个YunoHost实例发送邮件。如果你有以下情况,就很有用:你的25端口被你的ISP或VPS提供商封锁,你有一个住宅IP列在DUHL上,你不能配置反向DNS,或者这个服务器没有直接暴露在互联网上,你想使用其他服务器来发送邮件。",
+ "domain_cannot_remove_main_add_new_one": "你不能删除'{domain}',因为它是主域和你唯一的域,你需要先用'yunohost domain add '添加另一个域,然后用'yunohost domain main-domain -n '设置为主域,然后你可以用'yunohost domain remove {domain}'删除域",
+ "domain_cannot_add_xmpp_upload": "你不能添加以'xmpp-upload.'开头的域名。这种名称是为YunoHost中集成的XMPP上传功能保留的。",
+ "domain_cannot_remove_main": "你不能删除'{domain}',因为它是主域,你首先需要用'yunohost domain main-domain -n '设置另一个域作为主域;这里是候选域的列表: {other_domains}",
+ "diagnosis_sshd_config_inconsistent_details": "请运行yunohost settings set security.ssh.port -v YOUR_SSH_PORT 来定义SSH端口,并检查yunohost tools regen-conf ssh --dry-run --with-diff 和yunohost tools regen-conf ssh --force 将您的配置重置为YunoHost建议。",
+ "diagnosis_http_bad_status_code": "它看起来像另一台机器(也许是你的互联网路由器)回答,而不是你的服务器。
1。这个问题最常见的原因是80端口(和443端口)没有正确转发到您的服务器。
2.在更复杂的设置中:确保没有防火墙或反向代理的干扰。",
+ "diagnosis_http_timeout": "当试图从外部联系你的服务器时,出现了超时。它似乎是不可达的。
1. 这个问题最常见的原因是80端口(和443端口)没有正确转发到你的服务器。
2.你还应该确保nginx服务正在运行
3.对于更复杂的设置:确保没有防火墙或反向代理的干扰。",
+ "diagnosis_rootfstotalspace_critical": "根文件系统总共只有{space},这很令人担忧!您可能很快就会用完磁盘空间!建议根文件系统至少有16 GB。",
+ "diagnosis_rootfstotalspace_warning": "根文件系统总共只有{space}。这可能没问题,但要小心,因为最终您可能很快会用完磁盘空间...建议根文件系统至少有16 GB。",
+ "diagnosis_regenconf_manually_modified_details": "如果你知道自己在做什么的话,这可能是可以的! YunoHost会自动停止更新这个文件... 但是请注意,YunoHost的升级可能包含重要的推荐变化。如果你想,你可以用yunohost tools regen-conf {category} --dry-run --with-diff 检查差异,然后用yunohost tools regen-conf {category} --force 强制设置为推荐配置",
+ "diagnosis_mail_fcrdns_nok_alternatives_6": "有些供应商不会让你配置你的反向DNS(或者他们的功能可能被破坏......)。如果你的反向DNS正确配置为IPv4,你可以尝试在发送邮件时禁用IPv6,方法是运yunohost settings set smtp.allow_ipv6 -v off 。注意:这应视为最后一个解决方案因为这意味着你将无法从少数只使用IPv6的服务器发送或接收电子邮件。",
+ "diagnosis_mail_fcrdns_nok_alternatives_4": "有些供应商不会让你配置你的反向DNS(或者他们的功能可能被破坏......)。如果您因此而遇到问题,请考虑以下解决方案:
- 一些ISP提供了使用邮件服务器中转的选择,尽管这意味着中转将能够监视您的电子邮件流量。
- 一个有利于隐私的选择是使用VPN*与专用公共IP*来绕过这类限制。见https://yunohost.org/#/vpn_advantage
- 或者可以切换到另一个供应商",
+ "diagnosis_mail_ehlo_wrong_details": "远程诊断器在IPv{ipversion}中收到的EHLO与你的服务器的域名不同。
收到的EHLO: {wrong_ehlo}
预期的: {right_ehlo}
这个问题最常见的原因是端口25没有正确转发到你的服务器。另外,请确保没有防火墙或反向代理的干扰。",
+ "diagnosis_mail_ehlo_unreachable_details": "在IPv{ipversion}中无法打开与您服务器的25端口连接。它似乎是不可达的。
1. 这个问题最常见的原因是端口25没有正确转发到你的服务器。
2.你还应该确保postfix服务正在运行。
3.在更复杂的设置中:确保没有防火墙或反向代理的干扰。",
+ "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一些供应商不会让你解除对出站端口25的封锁,因为他们不关心网络中立性。
- 其中一些供应商提供了使用邮件服务器中继的替代方案,尽管这意味着中继将能够监视你的电子邮件流量。
- 一个有利于隐私的替代方案是使用VPN*,用一个专用的公共IP*绕过这种限制。见https://yunohost.org/#/vpn_advantage
- 你也可以考虑切换到一个更有利于网络中立的供应商",
+ "diagnosis_ram_ok": "系统在{total}中仍然有 {available} ({available_percent}%) RAM可用。",
+ "diagnosis_ram_low": "系统有 {available} ({available_percent}%) RAM可用(共{total}个)可用。小心。",
+ "diagnosis_ram_verylow": "系统只有 {available} ({available_percent}%) 内存可用! (在{total}中)",
+ "diagnosis_diskusage_ok": "存储器{mountpoint}
(在设备{device}
上)仍有 {free} ({free_percent}%) 空间(在{total}中)!",
+ "diagnosis_diskusage_low": "存储器{mountpoint}
(在设备{device}
上)只有{free} ({free_percent}%) 的空间。({free_percent}%)的剩余空间(在{total}中)。要小心。",
+ "diagnosis_diskusage_verylow": "存储器{mountpoint}
(在设备{device}
上)仅剩余{free} ({free_percent}%) (剩余{total})个空间。您应该真正考虑清理一些空间!",
+ "diagnosis_services_bad_status_tip": "你可以尝试重新启动服务,如果没有效果,可以看看webadmin中的服务日志(从命令行,你可以用yunohost service restart {service} 和yunohost service log {service} )来做。",
+ "diagnosis_dns_try_dyndns_update_force": "该域的DNS配置应由YunoHost自动管理,如果不是这种情况,您可以尝试使用 yunohost dyndns update --force 强制进行更新。",
+ "diagnosis_dns_point_to_doc": "如果您需要有关配置DNS记录的帮助,请查看 https://yunohost.org/dns_config 上的文档。",
+ "diagnosis_dns_discrepancy": "以下DNS记录似乎未遵循建议的配置:
类型: {type}
名称: {name}
代码> 当前值: {current}期望值: {value}
",
+ "log_backup_create": "创建备份档案",
+ "log_available_on_yunopaste": "现在可以通过{url}使用此日志",
+ "log_app_action_run": "运行 '{}' 应用的操作",
+ "log_app_makedefault": "将 '{}' 设为默认应用",
+ "log_app_upgrade": "升级 '{}' 应用",
+ "log_app_remove": "删除 '{}' 应用",
+ "log_app_install": "安装 '{}' 应用",
+ "log_app_change_url": "更改'{}'应用的网址",
+ "log_operation_unit_unclosed_properly": "操作单元未正确关闭",
+ "log_does_exists": "没有名称为'{log}'的操作日志,请使用 'yunohost log list' 查看所有可用的操作日志",
+ "log_help_to_get_failed_log": "操作'{desc}'无法完成。请使用命令'yunohost log share {name}' 共享此操作的完整日志以获取帮助",
+ "log_link_to_failed_log": "无法完成操作 '{desc}'。请通过单击此处提供此操作的完整日志以获取帮助",
+ "log_help_to_get_log": "要查看操作'{desc}'的日志,请使用命令'yunohost log show {name}'",
+ "log_link_to_log": "此操作的完整日志: '{desc}'",
+ "log_corrupted_md_file": "与日志关联的YAML元数据文件已损坏: '{md_file}\n错误: {error}'",
+ "iptables_unavailable": "你不能在这里使用iptables。你要么在一个容器中,要么你的内核不支持它",
+ "ip6tables_unavailable": "你不能在这里使用ip6tables。你要么在一个容器中,要么你的内核不支持它",
+ "log_regen_conf": "重新生成系统配置'{}'",
+ "log_letsencrypt_cert_renew": "续订'{}'的“Let's Encrypt”证书",
+ "log_selfsigned_cert_install": "在 '{}'域上安装自签名证书",
+ "log_permission_url": "更新与权限'{}'相关的网址",
+ "log_permission_delete": "删除权限'{}'",
+ "log_permission_create": "创建权限'{}'",
+ "log_letsencrypt_cert_install": "在'{}'域上安装“Let's Encrypt”证书",
+ "log_dyndns_update": "更新与您的YunoHost子域'{}'关联的IP",
+ "log_dyndns_subscribe": "订阅YunoHost子域'{}'",
+ "log_domain_remove": "从系统配置中删除 '{}' 域",
+ "log_domain_add": "将 '{}'域添加到系统配置中",
+ "log_remove_on_failed_install": "安装失败后删除 '{}'",
+ "log_remove_on_failed_restore": "从备份存档还原失败后,删除 '{}'",
+ "log_backup_restore_app": "从备份存档还原 '{}'",
+ "log_backup_restore_system": "从备份档案还原系统",
+ "permission_currently_allowed_for_all_users": "这个权限目前除了授予其他组以外,还授予所有用户。你可能想删除'all_users'权限或删除目前授予它的其他组。",
+ "permission_creation_failed": "无法创建权限'{permission}': {error}",
+ "permission_created": "权限'{permission}'已创建",
+ "permission_cannot_remove_main": "不允许删除主要权限",
+ "permission_already_up_to_date": "权限没有被更新,因为添加/删除请求已经符合当前状态。",
+ "permission_already_exist": "权限 '{permission}'已存在",
+ "permission_already_disallowed": "群组'{group}'已禁用权限'{permission}'",
+ "permission_already_allowed": "群组 '{group}' 已启用权限'{permission}'",
+ "pattern_password_app": "抱歉,密码不能包含以下字符: {forbidden_chars}",
+ "pattern_username": "只能为小写字母数字和下划线字符",
+ "pattern_port_or_range": "必须是有效的端口号(即0-65535)或端口范围(例如100:200)",
+ "pattern_password": "必须至少3个字符长",
+ "pattern_mailbox_quota": "必须为带b/k/M/G/T 后缀的大小或0,才能没有配额",
+ "pattern_lastname": "必须是有效的姓氏",
+ "pattern_firstname": "必须是有效的名字",
+ "pattern_email": "必须是有效的电子邮件地址,没有'+'符号(例如someone @ example.com)",
+ "pattern_email_forward": "必须是有效的电子邮件地址,接受 '+' 符号(例如someone + tag @ example.com)",
+ "pattern_domain": "必须是有效的域名(例如my-domain.org)",
+ "pattern_backup_archive_name": "必须是一个有效的文件名,最多30个字符,只有-_.和字母数字",
+ "password_too_simple_4": "密码长度至少为12个字符,并且包含数字,大写,小写和特殊字符",
+ "password_too_simple_3": "密码长度至少为8个字符,并且包含数字,大写,小写和特殊字符",
+ "password_too_simple_2": "密码长度至少为8个字符,并且包含数字,大写和小写字符",
+ "password_listed": "该密码是世界上最常用的密码之一。 请选择一些更独特的东西。",
+ "packages_upgrade_failed": "无法升级所有软件包",
+ "invalid_number": "必须是数字",
+ "not_enough_disk_space": "'{path}'上的可用空间不足",
+ "migrations_to_be_ran_manually": "迁移{id}必须手动运行。请转到webadmin页面上的工具→迁移,或运行`yunohost tools migrations run`。",
+ "migrations_success_forward": "迁移 {id} 已完成",
+ "migrations_skip_migration": "正在跳过迁移{id}...",
+ "migrations_running_forward": "正在运行迁移{id}...",
+ "migrations_pending_cant_rerun": "这些迁移仍处于待处理状态,因此无法再次运行: {ids}",
+ "migrations_not_pending_cant_skip": "这些迁移没有待处理,因此不能跳过: {ids}",
+ "migrations_no_such_migration": "没有称为 '{id}'的迁移",
+ "migrations_no_migrations_to_run": "无需迁移即可运行",
+ "migrations_need_to_accept_disclaimer": "要运行迁移{id},您必须接受以下免责声明:\n---\n{disclaimer}\n---\n如果您接受并继续运行迁移,请使用选项'--accept-disclaimer'重新运行该命令。",
+ "migrations_must_provide_explicit_targets": "使用'--skip'或'--force-rerun'时必须提供明确的目标",
+ "migrations_migration_has_failed": "迁移{id}尚未完成,正在中止。错误: {exception}",
+ "migrations_loading_migration": "正在加载迁移{id}...",
+ "migrations_list_conflict_pending_done": "您不能同时使用'--previous' 和'--done'。",
+ "migrations_exclusive_options": "'--auto', '--skip',和'--force-rerun'是互斥的选项。",
+ "migrations_failed_to_load_migration": "无法加载迁移{id}: {error}",
+ "migrations_dependencies_not_satisfied": "在迁移{id}之前运行以下迁移: '{dependencies_id}'。",
+ "migrations_cant_reach_migration_file": "无法访问路径'%s'处的迁移文件",
+ "migrations_already_ran": "这些迁移已经完成: {ids}",
+ "migration_0019_slapd_config_will_be_overwritten": "好像您手动编辑了slapd配置。对于此关键迁移,YunoHost需要强制更新slapd配置。原始文件将备份在{conf_backup_folder}中。",
+ "migration_0019_add_new_attributes_in_ldap": "在LDAP数据库中添加权限的新属性",
+ "migration_0018_failed_to_reset_legacy_rules": "无法重置旧版iptables规则: {error}",
+ "migration_0018_failed_to_migrate_iptables_rules": "无法将旧的iptables规则迁移到nftables: {error}",
+ "migration_0017_not_enough_space": "在{path}中提供足够的空间来运行迁移。",
+ "migration_0017_postgresql_11_not_installed": "已安装PostgreSQL 9.6,但未安装PostgreSQL11?您的系统上可能发生了一些奇怪的事情:(...",
+ "migration_0017_postgresql_96_not_installed": "PostgreSQL未安装在您的系统上。无事可做。",
+ "migration_0015_weak_certs": "发现以下证书仍然使用弱签名算法,并且必须升级以与下一版本的nginx兼容: {certs}",
+ "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团队竭尽全力对其进行检查和测试,但迁移仍可能会破坏系统或其应用程序的某些部分。\n\n因此,建议:\n -对任何关键数据或应用程序执行备份。有关更多信息,请访问https://yunohost.org/backup;\n -启动迁移后要耐心:根据您的Internet连接和硬件,升级所有内容最多可能需要几个小时。",
+ "migration_0015_system_not_fully_up_to_date": "您的系统不是最新的。请先执行常规升级,然后再运行向Buster的迁移。",
+ "migration_0015_not_enough_free_space": "/var/中的可用空间非常低!您应该至少有1GB的可用空间来运行此迁移。",
+ "migration_0015_not_stretch": "当前的Debian发行版不是Stretch!",
+ "migration_0015_yunohost_upgrade": "正在启动YunoHost核心升级...",
+ "migration_0015_still_on_stretch_after_main_upgrade": "在主要升级期间出了点问题,系统似乎仍在Debian Stretch上",
+ "migration_0015_main_upgrade": "正在开始主要升级...",
+ "migration_0015_patching_sources_list": "修补sources.lists ...",
+ "migration_0015_start": "开始迁移至Buster",
+ "migration_update_LDAP_schema": "正在更新LDAP模式...",
+ "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_description_0020_ssh_sftp_permissions": "添加SSH和SFTP权限支持",
+ "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",
+ "migration_description_0015_migrate_to_buster": "将系统升级到Debian Buster和YunoHost 4.x",
+ "migrating_legacy_permission_settings": "正在迁移旧版权限设置...",
+ "main_domain_changed": "主域已更改",
+ "main_domain_change_failed": "无法更改主域",
+ "mail_unavailable": "该电子邮件地址是保留的,并且将自动分配给第一个用户",
+ "mailbox_used_space_dovecot_down": "如果要获取使用过的邮箱空间,则必须启动Dovecot邮箱服务",
+ "mailbox_disabled": "用户{user}的电子邮件已关闭",
+ "mail_forward_remove_failed": "无法删除电子邮件转发'{mail}'",
+ "mail_domain_unknown": "域'{domain}'的电子邮件地址无效。请使用本服务器管理的域。",
+ "mail_alias_remove_failed": "无法删除电子邮件别名'{mail}'",
+ "log_tools_reboot": "重新启动服务器",
+ "log_tools_shutdown": "关闭服务器",
+ "log_tools_upgrade": "升级系统软件包",
+ "log_tools_postinstall": "安装好你的YunoHost服务器后",
+ "log_tools_migrations_migrate_forward": "运行迁移",
+ "log_domain_main_domain": "将 '{}' 设为主要域",
+ "log_user_permission_reset": "重置权限'{}'",
+ "log_user_permission_update": "更新权限'{}'的访问权限",
+ "log_user_update": "更新用户'{}'的信息",
+ "log_user_group_update": "更新组'{}'",
+ "log_user_group_delete": "删除组'{}'",
+ "log_user_group_create": "创建组'{}'",
+ "log_user_delete": "删除用户'{}'",
+ "log_user_create": "添加用户'{}'"
+}
\ No newline at end of file
diff --git a/pytest.ini b/pytest.ini
index 709e0e0b9..27d690435 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -3,12 +3,14 @@ addopts = -s -v
norecursedirs = dist doc build .tox .eggs
testpaths = tests/
markers =
- with_system_archive_from_2p4
+ with_system_archive_from_3p8
with_backup_recommended_app_installed
clean_opt_dir
- with_wordpress_archive_from_2p4
+ with_wordpress_archive_from_3p8
with_legacy_app_installed
with_backup_recommended_app_installed_with_ynh_restore
with_permission_app_installed
+ other_domains
+ with_custom_domain
filterwarnings =
ignore::urllib3.exceptions.InsecureRequestWarning
\ No newline at end of file
diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py
index 2f6d400c8..dad73e2a4 100644
--- a/src/yunohost/__init__.py
+++ b/src/yunohost/__init__.py
@@ -89,116 +89,73 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun
if not os.path.isdir(logdir):
os.makedirs(logdir, 0o750)
- # ####################################################################### #
+ logging_configuration = {
+ "version": 1,
+ "disable_existing_loggers": True,
+ "formatters": {
+ "console": {
+ "format": "%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s"
+ },
+ "tty-debug": {"format": "%(relativeCreated)-4d %(fmessage)s"},
+ "precise": {
+ "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s"
+ },
+ },
+ "filters": {
+ "action": {
+ "()": "moulinette.utils.log.ActionFilter",
+ },
+ },
+ "handlers": {
+ "cli": {
+ "level": "DEBUG" if debug else "INFO",
+ "class": "moulinette.interfaces.cli.TTYHandler",
+ "formatter": "tty-debug" if debug else "",
+ },
+ "api": {
+ "level": "DEBUG" if debug else "INFO",
+ "class": "moulinette.interfaces.api.APIQueueHandler",
+ },
+ "file": {
+ "class": "logging.FileHandler",
+ "formatter": "precise",
+ "filename": logfile,
+ "filters": ["action"],
+ },
+ },
+ "loggers": {
+ "yunohost": {
+ "level": "DEBUG",
+ "handlers": ["file", interface] if not quiet else ["file"],
+ "propagate": False,
+ },
+ "moulinette": {
+ "level": "DEBUG",
+ "handlers": ["file", interface] if not quiet else ["file"],
+ "propagate": False,
+ },
+ },
+ "root": {
+ "level": "DEBUG",
+ "handlers": ["file", interface] if debug else ["file"],
+ },
+ }
+
# Logging configuration for CLI (or any other interface than api...) #
- # ####################################################################### #
if interface != "api":
- configure_logging(
- {
- "version": 1,
- "main_logger": "yunohost",
- "disable_existing_loggers": True,
- "formatters": {
- "tty-debug": {"format": "%(relativeCreated)-4d %(fmessage)s"},
- "precise": {
- "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s"
- },
- },
- "filters": {
- "action": {
- "()": "moulinette.utils.log.ActionFilter",
- },
- },
- "handlers": {
- "tty": {
- "level": "DEBUG" if debug else "INFO",
- "class": "moulinette.interfaces.cli.TTYHandler",
- "formatter": "tty-debug" if debug else "",
- },
- "file": {
- "class": "logging.FileHandler",
- "formatter": "precise",
- "filename": logfile,
- "filters": ["action"],
- },
- },
- "loggers": {
- "yunohost": {
- "level": "DEBUG",
- "handlers": ["file", "tty"] if not quiet else ["file"],
- "propagate": False,
- },
- "moulinette": {
- "level": "DEBUG",
- "handlers": [],
- "propagate": True,
- },
- "moulinette.interface": {
- "level": "DEBUG",
- "handlers": ["file", "tty"] if not quiet else ["file"],
- "propagate": False,
- },
- },
- "root": {
- "level": "DEBUG",
- "handlers": ["file", "tty"] if debug else ["file"],
- },
- }
- )
- # ####################################################################### #
+ configure_logging(logging_configuration)
+
# Logging configuration for API #
- # ####################################################################### #
else:
- configure_logging(
- {
- "version": 1,
- "disable_existing_loggers": True,
- "formatters": {
- "console": {
- "format": "%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s"
- },
- "precise": {
- "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s"
- },
- },
- "filters": {
- "action": {
- "()": "moulinette.utils.log.ActionFilter",
- },
- },
- "handlers": {
- "api": {
- "level": "DEBUG" if debug else "INFO",
- "class": "moulinette.interfaces.api.APIQueueHandler",
- },
- "file": {
- "class": "logging.handlers.WatchedFileHandler",
- "formatter": "precise",
- "filename": logfile,
- "filters": ["action"],
- },
- "console": {
- "class": "logging.StreamHandler",
- "formatter": "console",
- "stream": "ext://sys.stdout",
- "filters": ["action"],
- },
- },
- "loggers": {
- "yunohost": {
- "level": "DEBUG",
- "handlers": ["file", "api"] + (["console"] if debug else []),
- "propagate": False,
- },
- "moulinette": {
- "level": "DEBUG",
- "handlers": [],
- "propagate": True,
- },
- },
- "root": {
- "level": "DEBUG",
- "handlers": ["file"] + (["console"] if debug else []),
- },
- }
- )
+ # We use a WatchedFileHandler instead of regular FileHandler to possibly support log rotation etc
+ logging_configuration["handlers"]["file"][
+ "class"
+ ] = "logging.handlers.WatchedFileHandler"
+
+ # This is for when launching yunohost-api in debug mode, we want to display stuff in the console
+ if debug:
+ logging_configuration["loggers"]["yunohost"]["handlers"].append("cli")
+ logging_configuration["loggers"]["moulinette"]["handlers"].append("cli")
+ logging_configuration["root"]["handlers"].append("cli")
+
+ configure_logging(logging_configuration)
diff --git a/src/yunohost/app.py b/src/yunohost/app.py
index 3d1d16f3c..fb544cab2 100644
--- a/src/yunohost/app.py
+++ b/src/yunohost/app.py
@@ -31,13 +31,12 @@ import yaml
import time
import re
import subprocess
-import glob
-import urllib.parse
+import tempfile
from collections import OrderedDict
+from typing import List, Tuple, Dict, Any
-from moulinette import msignals, m18n, msettings
+from moulinette import Moulinette, m18n
from moulinette.utils.log import getActionLogger
-from moulinette.utils.network import download_json
from moulinette.utils.process import run_commands, check_output
from moulinette.utils.filesystem import (
read_file,
@@ -46,108 +45,54 @@ from moulinette.utils.filesystem import (
read_yaml,
write_to_file,
write_to_json,
- write_to_yaml,
- chmod,
+ cp,
+ rm,
chown,
- mkdir,
+ chmod,
)
-from yunohost.service import service_status, _run_service_command
from yunohost.utils import packages
-from yunohost.utils.error import YunohostError
+from yunohost.utils.config import (
+ ConfigPanel,
+ ask_questions_and_parse_answers,
+ DomainQuestion,
+ PathQuestion,
+)
+from yunohost.utils.i18n import _value_for_locale
+from yunohost.utils.error import YunohostError, YunohostValidationError
+from yunohost.utils.filesystem import free_space_in_directory
from yunohost.log import is_unit_operation, OperationLogger
+from yunohost.app_catalog import ( # noqa
+ app_catalog,
+ app_search,
+ _load_apps_catalog,
+ app_fetchlist,
+)
logger = getActionLogger("yunohost.app")
-APPS_PATH = "/usr/share/yunohost/apps"
APPS_SETTING_PATH = "/etc/yunohost/apps/"
-INSTALL_TMP = "/var/cache/yunohost"
-APP_TMP_FOLDER = INSTALL_TMP + "/from_file"
-
-APPS_CATALOG_CACHE = "/var/cache/yunohost/repo"
-APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml"
-APPS_CATALOG_CRON_PATH = "/etc/cron.daily/yunohost-fetch-apps-catalog"
-APPS_CATALOG_API_VERSION = 2
-APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default"
+APP_TMP_WORKDIRS = "/var/cache/yunohost/app_tmp_work_dirs"
re_app_instance_name = re.compile(
r"^(?P[\w-]+?)(__(?P[1-9][0-9]*))?$"
)
+APP_REPO_URL = re.compile(
+ r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_.]+)?(\.git)?/?$"
+)
-def app_catalog(full=False, with_categories=False):
- """
- Return a dict of apps available to installation from Yunohost's app catalog
- """
-
- # Get app list from catalog cache
- catalog = _load_apps_catalog()
- installed_apps = set(_installed_apps())
-
- # Trim info for apps if not using --full
- for app, infos in catalog["apps"].items():
- infos["installed"] = app in installed_apps
-
- infos["manifest"]["description"] = _value_for_locale(
- infos["manifest"]["description"]
- )
-
- if not full:
- catalog["apps"][app] = {
- "description": infos["manifest"]["description"],
- "level": infos["level"],
- }
- else:
- infos["manifest"]["arguments"] = _set_default_ask_questions(
- infos["manifest"].get("arguments", {})
- )
-
- # Trim info for categories if not using --full
- for category in catalog["categories"]:
- category["title"] = _value_for_locale(category["title"])
- category["description"] = _value_for_locale(category["description"])
- for subtags in category.get("subtags", []):
- subtags["title"] = _value_for_locale(subtags["title"])
-
- if not full:
- catalog["categories"] = [
- {"id": c["id"], "description": c["description"]}
- for c in catalog["categories"]
- ]
-
- if not with_categories:
- return {"apps": catalog["apps"]}
- else:
- return {"apps": catalog["apps"], "categories": catalog["categories"]}
-
-
-def app_search(string):
- """
- Return a dict of apps whose description or name match the search string
- """
-
- # Retrieve a simple dict listing all apps
- catalog_of_apps = app_catalog()
-
- # Selecting apps according to a match in app name or description
- for app in catalog_of_apps["apps"].items():
- if not (
- re.search(string, app[0], flags=re.IGNORECASE)
- or re.search(string, app[1]["description"], flags=re.IGNORECASE)
- ):
- del catalog_of_apps["apps"][app[0]]
-
- return catalog_of_apps
-
-
-# Old legacy function...
-def app_fetchlist():
- logger.warning(
- "'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead"
- )
- from yunohost.tools import tools_update
-
- tools_update(apps=True)
+APP_FILES_TO_COPY = [
+ "manifest.json",
+ "manifest.toml",
+ "actions.json",
+ "actions.toml",
+ "config_panel.toml",
+ "scripts",
+ "conf",
+ "hooks",
+ "doc",
+]
def app_list(full=False, installed=False, filter=None):
@@ -191,13 +136,13 @@ def app_info(app, full=False):
"""
from yunohost.permission import user_permission_list
- if not _is_installed(app):
- raise YunohostError(
- "app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
- )
+ _assert_is_installed(app)
- local_manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app))
- permissions = user_permission_list(full=True, absolute_urls=True)["permissions"]
+ setting_path = os.path.join(APPS_SETTING_PATH, app)
+ local_manifest = _get_manifest_of_app(setting_path)
+ permissions = user_permission_list(full=True, absolute_urls=True, apps=[app])[
+ "permissions"
+ ]
settings = _get_app_settings(app)
@@ -213,6 +158,7 @@ def app_info(app, full=False):
if not full:
return ret
+ ret["setting_path"] = setting_path
ret["manifest"] = local_manifest
ret["manifest"]["arguments"] = _set_default_ask_questions(
ret["manifest"].get("arguments", {})
@@ -222,19 +168,23 @@ def app_info(app, full=False):
absolute_app_name, _ = _parse_app_instance_name(app)
ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {})
ret["upgradable"] = _app_upgradable(ret)
+
+ ret["is_webapp"] = "domain" in settings and "path" in settings
+
ret["supports_change_url"] = os.path.exists(
- os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")
+ os.path.join(setting_path, "scripts", "change_url")
)
ret["supports_backup_restore"] = os.path.exists(
- os.path.join(APPS_SETTING_PATH, app, "scripts", "backup")
- ) and os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "restore"))
+ os.path.join(setting_path, "scripts", "backup")
+ ) and os.path.exists(os.path.join(setting_path, "scripts", "restore"))
ret["supports_multi_instance"] = is_true(
local_manifest.get("multi_instance", False)
)
+ ret["supports_config_panel"] = os.path.exists(
+ os.path.join(setting_path, "config_panel.toml")
+ )
- ret["permissions"] = {
- p: i for p, i in permissions.items() if p.startswith(app + ".")
- }
+ ret["permissions"] = permissions
ret["label"] = permissions.get(app + ".main", {}).get("label")
if not ret["label"]:
@@ -257,8 +207,9 @@ def _app_upgradable(app_infos):
return "url_required"
# Do not advertise upgrades for bad-quality apps
+ level = app_in_catalog.get("level", -1)
if (
- not app_in_catalog.get("level", -1) >= 5
+ not (isinstance(level, int) and level >= 5)
or app_in_catalog.get("state") != "working"
):
return "bad_quality"
@@ -321,16 +272,18 @@ def app_map(app=None, raw=False, user=None):
if app is not None:
if not _is_installed(app):
- raise YunohostError(
+ raise YunohostValidationError(
"app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
)
apps = [
app,
]
else:
- apps = os.listdir(APPS_SETTING_PATH)
+ apps = _installed_apps()
- permissions = user_permission_list(full=True, absolute_urls=True)["permissions"]
+ permissions = user_permission_list(full=True, absolute_urls=True, apps=apps)[
+ "permissions"
+ ]
for app_id in apps:
app_settings = _get_app_settings(app_id)
if not app_settings:
@@ -418,41 +371,44 @@ def app_change_url(operation_logger, app, domain, path):
"""
from yunohost.hook import hook_exec, hook_callback
+ from yunohost.service import service_reload_or_restart
installed = _is_installed(app)
if not installed:
- raise YunohostError(
+ raise YunohostValidationError(
"app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
)
if not os.path.exists(
os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")
):
- raise YunohostError("app_change_url_no_script", app_name=app)
+ raise YunohostValidationError("app_change_url_no_script", app_name=app)
old_domain = app_setting(app, "domain")
old_path = app_setting(app, "path")
# Normalize path and domain format
- old_domain, old_path = _normalize_domain_path(old_domain, old_path)
- domain, path = _normalize_domain_path(domain, path)
+
+ domain = DomainQuestion.normalize(domain)
+ old_domain = DomainQuestion.normalize(old_domain)
+ path = PathQuestion.normalize(path)
+ old_path = PathQuestion.normalize(old_path)
if (domain, path) == (old_domain, old_path):
- raise YunohostError(
+ raise YunohostValidationError(
"app_change_url_identical_domains", domain=domain, path=path
)
- # Check the url is available
- _assert_no_conflicting_apps(domain, path, ignore_app=app)
+ app_setting_path = os.path.join(APPS_SETTING_PATH, app)
+ path_requirement = _guess_webapp_path_requirement(app_setting_path)
+ _validate_webpath_requirement(
+ {"domain": domain, "path": path}, path_requirement, ignore_app=app
+ )
- manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app))
-
- # Retrieve arguments list for change_url script
- # TODO: Allow to specify arguments
- args_odict = _parse_args_from_manifest(manifest, "change_url")
+ tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app)
# Prepare env. var. to pass to script
- env_dict = _make_environment_for_app_script(app, args=args_odict)
+ env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app)
env_dict["YNH_APP_OLD_DOMAIN"] = old_domain
env_dict["YNH_APP_OLD_PATH"] = old_path
env_dict["YNH_APP_NEW_DOMAIN"] = domain
@@ -463,34 +419,11 @@ def app_change_url(operation_logger, app, domain, path):
operation_logger.extra.update({"env": env_dict})
operation_logger.start()
- if os.path.exists(os.path.join(APP_TMP_FOLDER, "scripts")):
- shutil.rmtree(os.path.join(APP_TMP_FOLDER, "scripts"))
-
- shutil.copytree(
- os.path.join(APPS_SETTING_PATH, app, "scripts"),
- os.path.join(APP_TMP_FOLDER, "scripts"),
- )
-
- if os.path.exists(os.path.join(APP_TMP_FOLDER, "conf")):
- shutil.rmtree(os.path.join(APP_TMP_FOLDER, "conf"))
-
- shutil.copytree(
- os.path.join(APPS_SETTING_PATH, app, "conf"),
- os.path.join(APP_TMP_FOLDER, "conf"),
- )
+ change_url_script = os.path.join(tmp_workdir_for_app, "scripts/change_url")
# Execute App change_url script
- os.system("chown -R admin: %s" % INSTALL_TMP)
- os.system("chmod +x %s" % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts")))
- os.system(
- "chmod +x %s"
- % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))
- )
-
- if (
- hook_exec(os.path.join(APP_TMP_FOLDER, "scripts/change_url"), env=env_dict)[0]
- != 0
- ):
+ ret = hook_exec(change_url_script, env=env_dict)[0]
+ if ret != 0:
msg = "Failed to change '%s' url." % app
logger.error(msg)
operation_logger.error(msg)
@@ -500,6 +433,7 @@ def app_change_url(operation_logger, app, domain, path):
app_setting(app, "domain", value=old_domain)
app_setting(app, "path", value=old_path)
return
+ shutil.rmtree(tmp_workdir_for_app)
# this should idealy be done in the change_url script but let's avoid common mistakes
app_setting(app, "domain", value=domain)
@@ -507,22 +441,14 @@ def app_change_url(operation_logger, app, domain, path):
app_ssowatconf()
- # avoid common mistakes
- if _run_service_command("reload", "nginx") is False:
- # grab nginx errors
- # the "exit 0" is here to avoid check_output to fail because 'nginx -t'
- # will return != 0 since we are in a failed state
- nginx_errors = check_output("nginx -t; exit 0")
- raise YunohostError(
- "app_change_url_failed_nginx_reload", nginx_errors=nginx_errors
- )
+ service_reload_or_restart("nginx")
logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path))
hook_callback("post_app_change_url", env=env_dict)
-def app_upgrade(app=[], url=None, file=None, force=False):
+def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False):
"""
Upgrade app
@@ -530,14 +456,24 @@ def app_upgrade(app=[], url=None, file=None, force=False):
file -- Folder or tarball for upgrade
app -- App(s) to upgrade (default all)
url -- Git url to fetch for upgrade
+ no_safety_backup -- Disable the safety backup during upgrade
"""
from packaging import version
- from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
+ from yunohost.hook import (
+ hook_add,
+ hook_remove,
+ hook_callback,
+ hook_exec_with_script_debug_if_failure,
+ )
from yunohost.permission import permission_sync_to_user
from yunohost.regenconf import manually_modified_files
+ from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers
apps = app
+ # Check if disk space available
+ if free_space_in_directory("/") <= 512 * 1000 * 1000:
+ raise YunohostValidationError("disk_space_not_sufficient_update")
# If no app is specified, upgrade all apps
if not apps:
# FIXME : not sure what's supposed to happen if there is a url and a file but no apps...
@@ -550,13 +486,11 @@ def app_upgrade(app=[], url=None, file=None, force=False):
apps = [app_ for i, app_ in enumerate(apps) if app_ not in apps[:i]]
# Abort if any of those app is in fact not installed..
- for app in [app_ for app_ in apps if not _is_installed(app_)]:
- raise YunohostError(
- "app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
- )
+ for app_ in apps:
+ _assert_is_installed(app_)
if len(apps) == 0:
- raise YunohostError("apps_already_up_to_date")
+ raise YunohostValidationError("apps_already_up_to_date")
if len(apps) > 1:
logger.info(m18n.n("app_upgrade_several_apps", apps=", ".join(apps)))
@@ -567,22 +501,22 @@ def app_upgrade(app=[], url=None, file=None, force=False):
if file and isinstance(file, dict):
# We use this dirty hack to test chained upgrades in unit/functional tests
- manifest, extracted_app_folder = _extract_app_from_file(
- file[app_instance_name]
- )
+ new_app_src = file[app_instance_name]
elif file:
- manifest, extracted_app_folder = _extract_app_from_file(file)
+ new_app_src = file
elif url:
- manifest, extracted_app_folder = _fetch_app_from_git(url)
+ new_app_src = url
elif app_dict["upgradable"] == "url_required":
logger.warning(m18n.n("custom_app_url_required", app=app_instance_name))
continue
elif app_dict["upgradable"] == "yes" or force:
- manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name)
+ new_app_src = app_dict["manifest"]["id"]
else:
logger.success(m18n.n("app_already_up_to_date", app=app_instance_name))
continue
+ manifest, extracted_app_folder = _extract_app(new_app_src)
+
# Manage upgrade type and avoid any upgrade if there is nothing to do
upgrade_type = "UNKNOWN"
# Get current_version and new version
@@ -621,20 +555,19 @@ def app_upgrade(app=[], url=None, file=None, force=False):
upgrade_type = "UPGRADE_FULL"
# Check requirements
- _check_manifest_requirements(manifest, app_instance_name=app_instance_name)
+ _check_manifest_requirements(manifest)
_assert_system_is_sane_for_app(manifest, "pre")
- app_setting_path = APPS_SETTING_PATH + "/" + app_instance_name
-
- # Retrieve arguments list for upgrade script
- # TODO: Allow to specify arguments
- args_odict = _parse_args_from_manifest(manifest, "upgrade")
+ app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name)
# Prepare env. var. to pass to script
- env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict)
+ env_dict = _make_environment_for_app_script(
+ app_instance_name, workdir=extracted_app_folder
+ )
env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type
env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version)
env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version)
+ env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0"
# We'll check that the app didn't brutally edit some system configuration
manually_modified_files_before_install = manually_modified_files()
@@ -650,42 +583,21 @@ def app_upgrade(app=[], url=None, file=None, force=False):
operation_logger = OperationLogger("app_upgrade", related_to, env=env_dict)
operation_logger.start()
- # Execute App upgrade script
- os.system("chown -hR admin: %s" % INSTALL_TMP)
-
# Execute the app upgrade script
upgrade_failed = True
try:
- upgrade_retcode = hook_exec(
- extracted_app_folder + "/scripts/upgrade", env=env_dict
- )[0]
-
- upgrade_failed = True if upgrade_retcode != 0 else False
- if upgrade_failed:
- error = m18n.n("app_upgrade_script_failed")
- logger.error(
- m18n.n("app_upgrade_failed", app=app_instance_name, error=error)
- )
- failure_message_with_debug_instructions = operation_logger.error(error)
- if msettings.get("interface") != "api":
- dump_app_log_extract_for_debugging(operation_logger)
- # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
- except (KeyboardInterrupt, EOFError):
- upgrade_retcode = -1
- error = m18n.n("operation_interrupted")
- logger.error(
- m18n.n("app_upgrade_failed", app=app_instance_name, error=error)
+ (
+ upgrade_failed,
+ failure_message_with_debug_instructions,
+ ) = hook_exec_with_script_debug_if_failure(
+ extracted_app_folder + "/scripts/upgrade",
+ env=env_dict,
+ operation_logger=operation_logger,
+ error_message_if_script_failed=m18n.n("app_upgrade_script_failed"),
+ error_message_if_failed=lambda e: m18n.n(
+ "app_upgrade_failed", app=app_instance_name, error=e
+ ),
)
- failure_message_with_debug_instructions = operation_logger.error(error)
- # Something wrong happened in Yunohost's code (most probably hook_exec)
- except Exception:
- import traceback
-
- error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
- logger.error(
- m18n.n("app_install_failed", app=app_instance_name, error=error)
- )
- failure_message_with_debug_instructions = operation_logger.error(error)
finally:
# Whatever happened (install success or failure) we check if it broke the system
# and warn the user about it
@@ -745,40 +657,22 @@ def app_upgrade(app=[], url=None, file=None, force=False):
hook_add(app_instance_name, extracted_app_folder + "/hooks/" + hook)
# Replace scripts and manifest and conf (if exists)
- os.system(
- 'rm -rf "%s/scripts" "%s/manifest.toml %s/manifest.json %s/conf"'
- % (
- app_setting_path,
- app_setting_path,
- app_setting_path,
- app_setting_path,
- )
- )
-
- if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")):
- os.system(
- 'mv "%s/manifest.json" "%s/scripts" %s'
- % (extracted_app_folder, extracted_app_folder, app_setting_path)
- )
- if os.path.exists(os.path.join(extracted_app_folder, "manifest.toml")):
- os.system(
- 'mv "%s/manifest.toml" "%s/scripts" %s'
- % (extracted_app_folder, extracted_app_folder, app_setting_path)
- )
-
- for file_to_copy in [
- "actions.json",
- "actions.toml",
- "config_panel.json",
- "config_panel.toml",
- "conf",
- ]:
+ # Move scripts and manifest to the right place
+ for file_to_copy in APP_FILES_TO_COPY:
+ rm(f"{app_setting_path}/{file_to_copy}", recursive=True, force=True)
if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)):
- os.system(
- "cp -R %s/%s %s"
- % (extracted_app_folder, file_to_copy, app_setting_path)
+ cp(
+ f"{extracted_app_folder}/{file_to_copy}",
+ f"{app_setting_path}/{file_to_copy}",
+ recursive=True,
)
+ # Clean and set permissions
+ shutil.rmtree(extracted_app_folder)
+ chmod(app_setting_path, 0o600)
+ chmod(f"{app_setting_path}/settings.yml", 0o400)
+ chown(app_setting_path, "root", recursive=True)
+
# So much win
logger.success(m18n.n("app_upgraded", app=app_instance_name))
@@ -790,6 +684,15 @@ def app_upgrade(app=[], url=None, file=None, force=False):
logger.success(m18n.n("upgrade_complete"))
+def app_manifest(app):
+
+ manifest, extracted_app_folder = _extract_app(app)
+
+ shutil.rmtree(extracted_app_folder)
+
+ return manifest
+
+
@is_unit_operation()
def app_install(
operation_logger,
@@ -810,7 +713,13 @@ def app_install(
force -- Do not ask for confirmation when installing experimental / low-quality apps
"""
- from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
+ from yunohost.hook import (
+ hook_add,
+ hook_remove,
+ hook_callback,
+ hook_exec,
+ hook_exec_with_script_debug_if_failure,
+ )
from yunohost.log import OperationLogger
from yunohost.permission import (
user_permission_list,
@@ -819,85 +728,61 @@ def app_install(
permission_sync_to_user,
)
from yunohost.regenconf import manually_modified_files
+ from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers
- # Fetch or extract sources
- if not os.path.exists(INSTALL_TMP):
- os.makedirs(INSTALL_TMP)
+ # Check if disk space available
+ if free_space_in_directory("/") <= 512 * 1000 * 1000:
+ raise YunohostValidationError("disk_space_not_sufficient_install")
+
+ def confirm_install(app):
- def confirm_install(confirm):
# Ignore if there's nothing for confirm (good quality app), if --force is used
# or if request on the API (confirm already implemented on the API side)
- if confirm is None or force or msettings.get("interface") == "api":
+ if force or Moulinette.interface.type == "api":
return
- if confirm in ["danger", "thirdparty"]:
- answer = msignals.prompt(
- m18n.n("confirm_app_install_" + confirm, answers="Yes, I understand"),
+ quality = _app_quality(app)
+ if quality == "success":
+ return
+
+ # i18n: confirm_app_install_warning
+ # i18n: confirm_app_install_danger
+ # i18n: confirm_app_install_thirdparty
+
+ if quality in ["danger", "thirdparty"]:
+ answer = Moulinette.prompt(
+ m18n.n("confirm_app_install_" + quality, answers="Yes, I understand"),
color="red",
)
if answer != "Yes, I understand":
raise YunohostError("aborting")
else:
- answer = msignals.prompt(
- m18n.n("confirm_app_install_" + confirm, answers="Y/N"), color="yellow"
+ answer = Moulinette.prompt(
+ m18n.n("confirm_app_install_" + quality, answers="Y/N"), color="yellow"
)
if answer.upper() != "Y":
raise YunohostError("aborting")
- raw_app_list = _load_apps_catalog()["apps"]
-
- if app in raw_app_list or ("@" in app) or ("http://" in app) or ("https://" in app):
-
- # If we got an app name directly (e.g. just "wordpress"), we gonna test this name
- if app in raw_app_list:
- app_name_to_test = app
- # If we got an url like "https://github.com/foo/bar_ynh, we want to
- # extract "bar" and test if we know this app
- elif ("http://" in app) or ("https://" in app):
- app_name_to_test = app.strip("/").split("/")[-1].replace("_ynh", "")
- else:
- # FIXME : watdo if '@' in app ?
- app_name_to_test = None
-
- if app_name_to_test in raw_app_list:
-
- state = raw_app_list[app_name_to_test].get("state", "notworking")
- level = raw_app_list[app_name_to_test].get("level", None)
- confirm = "danger"
- if state in ["working", "validated"]:
- if isinstance(level, int) and level >= 5:
- confirm = None
- elif isinstance(level, int) and level > 0:
- confirm = "warning"
- else:
- confirm = "thirdparty"
-
- confirm_install(confirm)
-
- manifest, extracted_app_folder = _fetch_app_from_git(app)
- elif os.path.exists(app):
- confirm_install("thirdparty")
- manifest, extracted_app_folder = _extract_app_from_file(app)
- else:
- raise YunohostError("app_unknown")
+ confirm_install(app)
+ manifest, extracted_app_folder = _extract_app(app)
# Check ID
- if "id" not in manifest or "__" in manifest["id"]:
- raise YunohostError("app_id_invalid")
+ if "id" not in manifest or "__" in manifest["id"] or "." in manifest["id"]:
+ raise YunohostValidationError("app_id_invalid")
app_id = manifest["id"]
label = label if label else manifest["name"]
# Check requirements
- _check_manifest_requirements(manifest, app_id)
+ _check_manifest_requirements(manifest)
_assert_system_is_sane_for_app(manifest, "pre")
# Check if app can be forked
- instance_number = _installed_instance_number(app_id, last=True) + 1
+ instance_number = _next_instance_number_for_app(app_id)
if instance_number > 1:
if "multi_instance" not in manifest or not is_true(manifest["multi_instance"]):
- raise YunohostError("app_already_installed", app=app_id)
+ raise YunohostValidationError("app_already_installed", app=app_id)
# Change app_id to the forked app id
app_instance_name = app_id + "__" + str(instance_number)
@@ -905,13 +790,17 @@ def app_install(
app_instance_name = app_id
# Retrieve arguments list for install script
- args_dict = (
- {} if not args else dict(urllib.parse.parse_qsl(args, keep_blank_values=True))
- )
- args_odict = _parse_args_from_manifest(manifest, "install", args=args_dict)
+ raw_questions = manifest.get("arguments", {}).get("install", {})
+ questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args)
+ args = {
+ question.name: question.value
+ for question in questions
+ if question.value is not None
+ }
# Validate domain / path availability for webapps
- _validate_and_normalize_webpath(args_odict, extracted_app_folder)
+ path_requirement = _guess_webapp_path_requirement(extracted_app_folder)
+ _validate_webpath_requirement(args, path_requirement)
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(extracted_app_folder)
@@ -922,19 +811,6 @@ def app_install(
# We'll check that the app didn't brutally edit some system configuration
manually_modified_files_before_install = manually_modified_files()
- # Tell the operation_logger to redact all password-type args
- # Also redact the % escaped version of the password that might appear in
- # the 'args' section of metadata (relevant for password with non-alphanumeric char)
- data_to_redact = [
- value[0] for value in args_odict.values() if value[1] == "password"
- ]
- data_to_redact += [
- urllib.parse.quote(data)
- for data in data_to_redact
- if urllib.parse.quote(data) != data
- ]
- operation_logger.data_to_redact.extend(data_to_redact)
-
operation_logger.related_to = [
s for s in operation_logger.related_to if s[0] != "app"
]
@@ -957,28 +833,13 @@ def app_install(
}
_set_app_settings(app_instance_name, app_settings)
- os.system("chown -R admin: " + extracted_app_folder)
-
- # Execute App install script
- os.system("chown -hR admin: %s" % INSTALL_TMP)
# Move scripts and manifest to the right place
- if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")):
- os.system("cp %s/manifest.json %s" % (extracted_app_folder, app_setting_path))
- if os.path.exists(os.path.join(extracted_app_folder, "manifest.toml")):
- os.system("cp %s/manifest.toml %s" % (extracted_app_folder, app_setting_path))
- os.system("cp -R %s/scripts %s" % (extracted_app_folder, app_setting_path))
-
- for file_to_copy in [
- "actions.json",
- "actions.toml",
- "config_panel.json",
- "config_panel.toml",
- "conf",
- ]:
+ for file_to_copy in APP_FILES_TO_COPY:
if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)):
- os.system(
- "cp -R %s/%s %s"
- % (extracted_app_folder, file_to_copy, app_setting_path)
+ cp(
+ f"{extracted_app_folder}/{file_to_copy}",
+ f"{app_setting_path}/{file_to_copy}",
+ recursive=True,
)
# Initialize the main permission for the app
@@ -994,41 +855,33 @@ def app_install(
)
# Prepare env. var. to pass to script
- env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict)
+ env_dict = _make_environment_for_app_script(
+ app_instance_name, args=args, workdir=extracted_app_folder
+ )
env_dict_for_logging = env_dict.copy()
- for arg_name, arg_value_and_type in args_odict.items():
- if arg_value_and_type[1] == "password":
- del env_dict_for_logging["YNH_APP_ARG_%s" % arg_name.upper()]
+ for question in questions:
+ # Or should it be more generally question.redact ?
+ if question.type == "password":
+ del env_dict_for_logging["YNH_APP_ARG_%s" % question.name.upper()]
operation_logger.extra.update({"env": env_dict_for_logging})
# Execute the app install script
install_failed = True
try:
- install_retcode = hook_exec(
- os.path.join(extracted_app_folder, "scripts/install"), env=env_dict
- )[0]
- # "Common" app install failure : the script failed and returned exit code != 0
- install_failed = True if install_retcode != 0 else False
- if install_failed:
- error = m18n.n("app_install_script_failed")
- logger.error(m18n.n("app_install_failed", app=app_id, error=error))
- failure_message_with_debug_instructions = operation_logger.error(error)
- if msettings.get("interface") != "api":
- dump_app_log_extract_for_debugging(operation_logger)
- # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
- except (KeyboardInterrupt, EOFError):
- error = m18n.n("operation_interrupted")
- logger.error(m18n.n("app_install_failed", app=app_id, error=error))
- failure_message_with_debug_instructions = operation_logger.error(error)
- # Something wrong happened in Yunohost's code (most probably hook_exec)
- except Exception:
- import traceback
-
- error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
- logger.error(m18n.n("app_install_failed", app=app_id, error=error))
- failure_message_with_debug_instructions = operation_logger.error(error)
+ (
+ install_failed,
+ failure_message_with_debug_instructions,
+ ) = hook_exec_with_script_debug_if_failure(
+ os.path.join(extracted_app_folder, "scripts/install"),
+ env=env_dict,
+ operation_logger=operation_logger,
+ error_message_if_script_failed=m18n.n("app_install_script_failed"),
+ error_message_if_failed=lambda e: m18n.n(
+ "app_install_failed", app=app_id, error=e
+ ),
+ )
finally:
# If success so far, validate that app didn't break important stuff
if not install_failed:
@@ -1065,11 +918,9 @@ def app_install(
logger.warning(m18n.n("app_remove_after_failed_install"))
# Setup environment for remove script
- env_dict_remove = {}
- env_dict_remove["YNH_APP_ID"] = app_id
- env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name
- env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
- env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?")
+ env_dict_remove = _make_environment_for_app_script(
+ app_instance_name, workdir=extracted_app_folder
+ )
# Execute remove script
operation_logger_remove = OperationLogger(
@@ -1122,11 +973,7 @@ def app_install(
permission_sync_to_user()
- raise YunohostError(
- failure_message_with_debug_instructions,
- raw_msg=True,
- log_ref=operation_logger.name,
- )
+ raise YunohostError(failure_message_with_debug_instructions, raw_msg=True)
# Clean hooks and add new ones
hook_remove(app_instance_name)
@@ -1136,71 +983,26 @@ def app_install(
# Clean and set permissions
shutil.rmtree(extracted_app_folder)
- os.system("chmod -R 400 %s" % app_setting_path)
- os.system("chown -R root: %s" % app_setting_path)
- os.system("chown -R admin: %s/scripts" % app_setting_path)
+ chmod(app_setting_path, 0o600)
+ chmod(f"{app_setting_path}/settings.yml", 0o400)
+ chown(app_setting_path, "root", recursive=True)
logger.success(m18n.n("installation_complete"))
hook_callback("post_app_install", env=env_dict)
-def dump_app_log_extract_for_debugging(operation_logger):
-
- with open(operation_logger.log_path, "r") as f:
- lines = f.readlines()
-
- filters = [
- r"set [+-]x$",
- r"set [+-]o xtrace$",
- r"local \w+$",
- r"local legacy_args=.*$",
- r".*Helper used in legacy mode.*",
- r"args_array=.*$",
- r"local -A args_array$",
- r"ynh_handle_getopts_args",
- r"ynh_script_progression",
- ]
-
- filters = [re.compile(f_) for f_ in filters]
-
- lines_to_display = []
- for line in lines:
-
- if ": " not in line.strip():
- continue
-
- # A line typically looks like
- # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo
- # And we just want the part starting by "DEBUG - "
- line = line.strip().split(": ", 1)[1]
-
- if any(filter_.search(line) for filter_ in filters):
- continue
-
- lines_to_display.append(line)
-
- if line.endswith("+ ynh_exit_properly") or " + ynh_die " in line:
- break
- elif len(lines_to_display) > 20:
- lines_to_display.pop(0)
-
- logger.warning(
- "Here's an extract of the logs before the crash. It might help debugging the error:"
- )
- for line in lines_to_display:
- logger.info(line)
-
-
@is_unit_operation()
-def app_remove(operation_logger, app):
+def app_remove(operation_logger, app, purge=False):
"""
Remove app
- Keyword argument:
+ Keyword arguments:
app -- App(s) to delete
+ purge -- Remove with all app data
"""
+ from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers
from yunohost.hook import hook_exec, hook_remove, hook_callback
from yunohost.permission import (
user_permission_list,
@@ -1209,7 +1011,7 @@ def app_remove(operation_logger, app):
)
if not _is_installed(app):
- raise YunohostError(
+ raise YunohostValidationError(
"app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
)
@@ -1217,13 +1019,7 @@ def app_remove(operation_logger, app):
logger.info(m18n.n("app_start_remove", app=app))
- app_setting_path = APPS_SETTING_PATH + app
-
- # TODO: display fail messages from script
- try:
- shutil.rmtree("/tmp/yunohost_remove")
- except Exception:
- pass
+ app_setting_path = os.path.join(APPS_SETTING_PATH, app)
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(app_setting_path)
@@ -1233,25 +1029,19 @@ def app_remove(operation_logger, app):
_patch_legacy_php_versions(app_setting_path)
manifest = _get_manifest_of_app(app_setting_path)
-
- os.system(
- "cp -a %s /tmp/yunohost_remove && chown -hR admin: /tmp/yunohost_remove"
- % app_setting_path
- )
- os.system("chown -R admin: /tmp/yunohost_remove")
- os.system("chmod -R u+rX /tmp/yunohost_remove")
+ tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app)
+ remove_script = f"{tmp_workdir_for_app}/scripts/remove"
env_dict = {}
app_id, app_instance_nb = _parse_app_instance_name(app)
- env_dict["YNH_APP_ID"] = app_id
- env_dict["YNH_APP_INSTANCE_NAME"] = app
- env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb)
- env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?")
+ env_dict = _make_environment_for_app_script(app, workdir=tmp_workdir_for_app)
+ env_dict["YNH_APP_PURGE"] = str(1 if purge else 0)
+
operation_logger.extra.update({"env": env_dict})
operation_logger.flush()
try:
- ret = hook_exec("/tmp/yunohost_remove/scripts/remove", env=env_dict)[0]
+ ret = hook_exec(remove_script, env=env_dict)[0]
# Here again, calling hook_exec could fail miserably, or get
# manually interrupted (by mistake or because script was stuck)
# In that case we still want to proceed with the rest of the
@@ -1261,6 +1051,8 @@ def app_remove(operation_logger, app):
import traceback
logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc()))
+ finally:
+ shutil.rmtree(tmp_workdir_for_app)
if ret == 0:
logger.success(m18n.n("app_removed", app=app))
@@ -1268,15 +1060,14 @@ def app_remove(operation_logger, app):
else:
logger.warning(m18n.n("app_not_properly_removed", app=app))
+ # Remove all permission in LDAP
+ for permission_name in user_permission_list(apps=[app])["permissions"].keys():
+ permission_delete(permission_name, force=True, sync_perm=False)
+
if os.path.exists(app_setting_path):
shutil.rmtree(app_setting_path)
- shutil.rmtree("/tmp/yunohost_remove")
- hook_remove(app)
- # Remove all permission in LDAP
- for permission_name in user_permission_list()["permissions"].keys():
- if permission_name.startswith(app + "."):
- permission_delete(permission_name, force=True, sync_perm=False)
+ hook_remove(app)
permission_sync_to_user()
_assert_system_is_sane_for_app(manifest, "post")
@@ -1293,10 +1084,6 @@ def app_addaccess(apps, users=[]):
"""
from yunohost.permission import user_permission_update
- logger.warning(
- "/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions."
- )
-
output = {}
for app in apps:
permission = user_permission_update(
@@ -1318,10 +1105,6 @@ def app_removeaccess(apps, users=[]):
"""
from yunohost.permission import user_permission_update
- logger.warning(
- "/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions."
- )
-
output = {}
for app in apps:
permission = user_permission_update(app + ".main", remove=users)
@@ -1340,10 +1123,6 @@ def app_clearaccess(apps):
"""
from yunohost.permission import user_permission_reset
- logger.warning(
- "/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions."
- )
-
output = {}
for app in apps:
permission = user_permission_reset(app + ".main")
@@ -1362,7 +1141,7 @@ def app_makedefault(operation_logger, app, domain=None):
domain
"""
- from yunohost.domain import domain_list
+ from yunohost.domain import _assert_domain_exists
app_settings = _get_app_settings(app)
app_domain = app_settings["domain"]
@@ -1370,12 +1149,13 @@ def app_makedefault(operation_logger, app, domain=None):
if domain is None:
domain = app_domain
- operation_logger.related_to.append(("domain", domain))
- elif domain not in domain_list()["domains"]:
- raise YunohostError("domain_name_unknown", domain=domain)
+
+ _assert_domain_exists(domain)
+
+ operation_logger.related_to.append(("domain", domain))
if "/" in app_map(raw=True)[domain]:
- raise YunohostError(
+ raise YunohostValidationError(
"app_make_default_location_already_used",
app=app,
domain=app_domain,
@@ -1401,7 +1181,7 @@ def app_makedefault(operation_logger, app, domain=None):
write_to_json(
"/etc/ssowat/conf.json.persistent", ssowat_conf, sort_keys=True, indent=4
)
- os.system("chmod 644 /etc/ssowat/conf.json.persistent")
+ chmod("/etc/ssowat/conf.json.persistent", 0o644)
logger.success(m18n.n("ssowat_conf_updated"))
@@ -1438,7 +1218,7 @@ def app_setting(app, key, value=None, delete=False):
permission_url,
)
- permissions = user_permission_list(full=True)["permissions"]
+ permissions = user_permission_list(full=True, apps=[app])["permissions"]
permission_name = "%s.legacy_%s_uris" % (app, key.split("_")[0])
permission = permissions.get(permission_name)
@@ -1471,9 +1251,6 @@ def app_setting(app, key, value=None, delete=False):
# SET
else:
- logger.warning(
- "/!\\ Packagers! This app is still using the skipped/protected/unprotected_uris/regex settings which are now obsolete and deprecated... Instead, you should use the new helpers 'ynh_permission_{create,urls,update,delete}' and the 'visitors' group to initialize the public/private access. Check out the documentation at the bottom of yunohost.org/groups_and_permissions to learn how to use the new permission mechanism."
- )
urls = value
# If the request is about the root of the app (/), ( = the vast majority of cases)
@@ -1549,7 +1326,7 @@ def app_setting(app, key, value=None, delete=False):
# SET
else:
if key in ["redirected_urls", "redirected_regex"]:
- value = yaml.load(value)
+ value = yaml.safe_load(value)
app_settings[key] = value
_set_app_settings(app, app_settings)
@@ -1570,7 +1347,8 @@ def app_register_url(app, domain, path):
permission_sync_to_user,
)
- domain, path = _normalize_domain_path(domain, path)
+ domain = DomainQuestion.normalize(domain)
+ path = PathQuestion.normalize(path)
# We cannot change the url of an app already installed simply by changing
# the settings...
@@ -1578,7 +1356,7 @@ def app_register_url(app, domain, path):
if _is_installed(app):
settings = _get_app_settings(app)
if "path" in settings.keys() and "domain" in settings.keys():
- raise YunohostError("app_already_installed_cant_change_url")
+ raise YunohostValidationError("app_already_installed_cant_change_url")
# Check the url is available
_assert_no_conflicting_apps(domain, path)
@@ -1694,7 +1472,7 @@ def app_change_label(app, new_label):
installed = _is_installed(app)
if not installed:
- raise YunohostError(
+ raise YunohostValidationError(
"app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
)
logger.warning(m18n.n("app_label_deprecated"))
@@ -1723,14 +1501,13 @@ def app_action_run(operation_logger, app, action, args=None):
logger.warning(m18n.n("experimental_feature"))
from yunohost.hook import hook_exec
- import tempfile
# will raise if action doesn't exist
actions = app_action_list(app)["actions"]
actions = {x["id"]: x for x in actions}
if action not in actions:
- raise YunohostError(
+ raise YunohostValidationError(
"action '%s' not available for app '%s', available actions are: %s"
% (action, app, ", ".join(actions.keys())),
raw_msg=True,
@@ -1741,34 +1518,48 @@ def app_action_run(operation_logger, app, action, args=None):
action_declaration = actions[action]
# Retrieve arguments list for install script
- args_dict = (
- dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {}
- )
- args_odict = _parse_args_for_action(actions[action], args=args_dict)
+ raw_questions = actions[action].get("arguments", {})
+ questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args)
+ args = {
+ question.name: question.value
+ for question in questions
+ if question.value is not None
+ }
+
+ tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app)
env_dict = _make_environment_for_app_script(
- app, args=args_odict, args_prefix="ACTION_"
+ app, args=args, args_prefix="ACTION_", workdir=tmp_workdir_for_app
)
env_dict["YNH_ACTION"] = action
- _, path = tempfile.mkstemp()
+ _, action_script = tempfile.mkstemp(dir=tmp_workdir_for_app)
- with open(path, "w") as script:
+ with open(action_script, "w") as script:
script.write(action_declaration["command"])
- os.chmod(path, 700)
-
if action_declaration.get("cwd"):
cwd = action_declaration["cwd"].replace("$app", app)
else:
- cwd = "/etc/yunohost/apps/" + app
+ cwd = tmp_workdir_for_app
- retcode = hook_exec(
- path,
- env=env_dict,
- chdir=cwd,
- user=action_declaration.get("user", "root"),
- )[0]
+ try:
+ retcode = hook_exec(
+ action_script,
+ env=env_dict,
+ chdir=cwd,
+ user=action_declaration.get("user", "root"),
+ )[0]
+ # Calling hook_exec could fail miserably, or get
+ # manually interrupted (by mistake or because script was stuck)
+ # In that case we still want to delete the tmp work dir
+ except (KeyboardInterrupt, EOFError, Exception):
+ retcode = -1
+ import traceback
+
+ logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc()))
+ finally:
+ shutil.rmtree(tmp_workdir_for_app)
if retcode not in action_declaration.get("accepted_return_codes", [0]):
msg = "Error while executing action '%s' of app '%s': return code %s" % (
@@ -1779,191 +1570,106 @@ def app_action_run(operation_logger, app, action, args=None):
operation_logger.error(msg)
raise YunohostError(msg, raw_msg=True)
- os.remove(path)
-
operation_logger.success()
return logger.success("Action successed!")
-# Config panel todo list:
-# * docstrings
-# * merge translations on the json once the workflow is in place
-@is_unit_operation()
-def app_config_show_panel(operation_logger, app):
- logger.warning(m18n.n("experimental_feature"))
-
- from yunohost.hook import hook_exec
-
- # this will take care of checking if the app is installed
- app_info_dict = app_info(app)
-
- operation_logger.start()
- config_panel = _get_app_config_panel(app)
- config_script = os.path.join(APPS_SETTING_PATH, app, "scripts", "config")
-
- app_id, app_instance_nb = _parse_app_instance_name(app)
-
- if not config_panel or not os.path.exists(config_script):
- return {
- "app_id": app_id,
- "app": app,
- "app_name": app_info_dict["name"],
- "config_panel": [],
- }
-
- env = {
- "YNH_APP_ID": app_id,
- "YNH_APP_INSTANCE_NAME": app,
- "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb),
- }
-
- return_code, parsed_values = hook_exec(
- config_script, args=["show"], env=env, return_format="plain_dict"
- )
-
- if return_code != 0:
- raise Exception(
- "script/config show return value code: %s (considered as an error)",
- return_code,
+def app_config_get(app, key="", full=False, export=False):
+ """
+ Display an app configuration in classic, full or export mode
+ """
+ if full and export:
+ raise YunohostValidationError(
+ "You can't use --full and --export together.", raw_msg=True
)
- logger.debug("Generating global variables:")
- for tab in config_panel.get("panel", []):
- tab_id = tab["id"] # this makes things easier to debug on crash
- for section in tab.get("sections", []):
- section_id = section["id"]
- for option in section.get("options", []):
- option_name = option["name"]
- generated_name = (
- "YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name)
- ).upper()
- option["name"] = generated_name
- logger.debug(
- " * '%s'.'%s'.'%s' -> %s",
- tab.get("name"),
- section.get("name"),
- option.get("name"),
- generated_name,
+ if full:
+ mode = "full"
+ elif export:
+ mode = "export"
+ else:
+ mode = "classic"
+
+ config_ = AppConfigPanel(app)
+ return config_.get(key, mode)
+
+
+@is_unit_operation()
+def app_config_set(
+ operation_logger, app, key=None, value=None, args=None, args_file=None
+):
+ """
+ Apply a new app configuration
+ """
+
+ config_ = AppConfigPanel(app)
+
+ return config_.set(key, value, args, args_file, operation_logger=operation_logger)
+
+
+class AppConfigPanel(ConfigPanel):
+ def __init__(self, app):
+
+ # Check app is installed
+ _assert_is_installed(app)
+
+ self.app = app
+ config_path = os.path.join(APPS_SETTING_PATH, app, "config_panel.toml")
+ super().__init__(config_path=config_path)
+
+ def _load_current_values(self):
+ self.values = self._call_config_script("show")
+
+ def _apply(self):
+ env = {key: str(value) for key, value in self.new_values.items()}
+ return_content = self._call_config_script("apply", env=env)
+
+ # If the script returned validation error
+ # raise a ValidationError exception using
+ # the first key
+ if return_content:
+ for key, message in return_content.get("validation_errors").items():
+ raise YunohostValidationError(
+ "app_argument_invalid",
+ name=key,
+ error=message,
)
- if generated_name in parsed_values:
- # code is not adapted for that so we have to mock expected format :/
- if option.get("type") == "boolean":
- if parsed_values[generated_name].lower() in ("true", "1", "y"):
- option["default"] = parsed_values[generated_name]
- else:
- del option["default"]
- else:
- option["default"] = parsed_values[generated_name]
+ def _call_config_script(self, action, env={}):
+ from yunohost.hook import hook_exec
- args_dict = _parse_args_in_yunohost_format(
- {option["name"]: parsed_values[generated_name]}, [option]
- )
- option["default"] = args_dict[option["name"]][0]
- else:
- logger.debug(
- "Variable '%s' is not declared by config script, using default",
- generated_name,
- )
- # do nothing, we'll use the default if present
+ # Add default config script if needed
+ config_script = os.path.join(APPS_SETTING_PATH, self.app, "scripts", "config")
+ if not os.path.exists(config_script):
+ logger.debug("Adding a default config script")
+ default_script = """#!/bin/bash
+source /usr/share/yunohost/helpers
+ynh_abort_if_errors
+ynh_app_config_run $1
+"""
+ write_to_file(config_script, default_script)
- return {
- "app_id": app_id,
- "app": app,
- "app_name": app_info_dict["name"],
- "config_panel": config_panel,
- "logs": operation_logger.success(),
- }
-
-
-@is_unit_operation()
-def app_config_apply(operation_logger, app, args):
- logger.warning(m18n.n("experimental_feature"))
-
- from yunohost.hook import hook_exec
-
- installed = _is_installed(app)
- if not installed:
- raise YunohostError(
- "app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
+ # Call config script to extract current values
+ logger.debug(f"Calling '{action}' action from config script")
+ app_id, app_instance_nb = _parse_app_instance_name(self.app)
+ settings = _get_app_settings(app_id)
+ env.update(
+ {
+ "app_id": app_id,
+ "app": self.app,
+ "app_instance_nb": str(app_instance_nb),
+ "final_path": settings.get("final_path", ""),
+ "YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, self.app),
+ }
)
- config_panel = _get_app_config_panel(app)
- config_script = os.path.join(APPS_SETTING_PATH, app, "scripts", "config")
-
- if not config_panel or not os.path.exists(config_script):
- # XXX real exception
- raise Exception("Not config-panel.json nor scripts/config")
-
- operation_logger.start()
- app_id, app_instance_nb = _parse_app_instance_name(app)
- env = {
- "YNH_APP_ID": app_id,
- "YNH_APP_INSTANCE_NAME": app,
- "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb),
- }
- args = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {}
-
- for tab in config_panel.get("panel", []):
- tab_id = tab["id"] # this makes things easier to debug on crash
- for section in tab.get("sections", []):
- section_id = section["id"]
- for option in section.get("options", []):
- option_name = option["name"]
- generated_name = (
- "YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name)
- ).upper()
-
- if generated_name in args:
- logger.debug(
- "include into env %s=%s", generated_name, args[generated_name]
- )
- env[generated_name] = args[generated_name]
- else:
- logger.debug("no value for key id %s", generated_name)
-
- # for debug purpose
- for key in args:
- if key not in env:
- logger.warning(
- "Ignore key '%s' from arguments because it is not in the config", key
- )
-
- return_code = hook_exec(
- config_script,
- args=["apply"],
- env=env,
- )[0]
-
- if return_code != 0:
- msg = (
- "'script/config apply' return value code: %s (considered as an error)"
- % return_code
- )
- operation_logger.error(msg)
- raise Exception(msg)
-
- logger.success("Config updated as expected")
- return {
- "app": app,
- "logs": operation_logger.success(),
- }
-
-
-def _get_all_installed_apps_id():
- """
- Return something like:
- ' * app1
- * app2
- * ...'
- """
-
- all_apps_ids = sorted(_installed_apps())
-
- all_apps_ids_formatted = "\n * ".join(all_apps_ids)
- all_apps_ids_formatted = "\n * " + all_apps_ids_formatted
-
- return all_apps_ids_formatted
+ ret, values = hook_exec(config_script, args=[action], env=env)
+ if ret != 0:
+ if action == "show":
+ raise YunohostError("app_config_unable_to_read")
+ else:
+ raise YunohostError("app_config_unable_to_apply")
+ return values
def _get_app_actions(app_id):
@@ -2051,145 +1757,6 @@ def _get_app_actions(app_id):
return None
-def _get_app_config_panel(app_id):
- "Get app config panel stored in json or in toml"
- config_panel_toml_path = os.path.join(
- APPS_SETTING_PATH, app_id, "config_panel.toml"
- )
- config_panel_json_path = os.path.join(
- APPS_SETTING_PATH, app_id, "config_panel.json"
- )
-
- # sample data to get an idea of what is going on
- # this toml extract:
- #
- # version = "0.1"
- # name = "Unattended-upgrades configuration panel"
- #
- # [main]
- # name = "Unattended-upgrades configuration"
- #
- # [main.unattended_configuration]
- # name = "50unattended-upgrades configuration file"
- #
- # [main.unattended_configuration.upgrade_level]
- # name = "Choose the sources of packages to automatically upgrade."
- # default = "Security only"
- # type = "text"
- # help = "We can't use a choices field for now. In the meantime please choose between one of this values:
Security only, Security and updates."
- # # choices = ["Security only", "Security and updates"]
-
- # [main.unattended_configuration.ynh_update]
- # name = "Would you like to update YunoHost packages automatically ?"
- # type = "bool"
- # default = true
- #
- # will be parsed into this:
- #
- # OrderedDict([(u'version', u'0.1'),
- # (u'name', u'Unattended-upgrades configuration panel'),
- # (u'main',
- # OrderedDict([(u'name', u'Unattended-upgrades configuration'),
- # (u'unattended_configuration',
- # OrderedDict([(u'name',
- # u'50unattended-upgrades configuration file'),
- # (u'upgrade_level',
- # OrderedDict([(u'name',
- # u'Choose the sources of packages to automatically upgrade.'),
- # (u'default',
- # u'Security only'),
- # (u'type', u'text'),
- # (u'help',
- # u"We can't use a choices field for now. In the meantime please choose between one of this values:
Security only, Security and updates.")])),
- # (u'ynh_update',
- # OrderedDict([(u'name',
- # u'Would you like to update YunoHost packages automatically ?'),
- # (u'type', u'bool'),
- # (u'default', True)])),
- #
- # and needs to be converted into this:
- #
- # {u'name': u'Unattended-upgrades configuration panel',
- # u'panel': [{u'id': u'main',
- # u'name': u'Unattended-upgrades configuration',
- # u'sections': [{u'id': u'unattended_configuration',
- # u'name': u'50unattended-upgrades configuration file',
- # u'options': [{u'//': u'"choices" : ["Security only", "Security and updates"]',
- # u'default': u'Security only',
- # u'help': u"We can't use a choices field for now. In the meantime please choose between one of this values:
Security only, Security and updates.",
- # u'id': u'upgrade_level',
- # u'name': u'Choose the sources of packages to automatically upgrade.',
- # u'type': u'text'},
- # {u'default': True,
- # u'id': u'ynh_update',
- # u'name': u'Would you like to update YunoHost packages automatically ?',
- # u'type': u'bool'},
-
- if os.path.exists(config_panel_toml_path):
- toml_config_panel = toml.load(
- open(config_panel_toml_path, "r"), _dict=OrderedDict
- )
-
- # transform toml format into json format
- config_panel = {
- "name": toml_config_panel["name"],
- "version": toml_config_panel["version"],
- "panel": [],
- }
-
- panels = [
- key_value
- for key_value in toml_config_panel.items()
- if key_value[0] not in ("name", "version")
- and isinstance(key_value[1], OrderedDict)
- ]
-
- for key, value in panels:
- panel = {
- "id": key,
- "name": value["name"],
- "sections": [],
- }
-
- sections = [
- k_v1
- for k_v1 in value.items()
- if k_v1[0] not in ("name",) and isinstance(k_v1[1], OrderedDict)
- ]
-
- for section_key, section_value in sections:
- section = {
- "id": section_key,
- "name": section_value["name"],
- "options": [],
- }
-
- options = [
- k_v
- for k_v in section_value.items()
- if k_v[0] not in ("name",) and isinstance(k_v[1], OrderedDict)
- ]
-
- for option_key, option_value in options:
- option = dict(option_value)
- option["name"] = option_key
- option["ask"] = {"en": option["ask"]}
- if "help" in option:
- option["help"] = {"en": option["help"]}
- section["options"].append(option)
-
- panel["sections"].append(section)
-
- config_panel["panel"].append(panel)
-
- return config_panel
-
- elif os.path.exists(config_panel_json_path):
- return json.load(open(config_panel_json_path))
-
- return None
-
-
def _get_app_settings(app_id):
"""
Get settings of an installed app
@@ -2199,12 +1766,12 @@ def _get_app_settings(app_id):
"""
if not _is_installed(app_id):
- raise YunohostError(
+ raise YunohostValidationError(
"app_not_installed", app=app_id, all_apps=_get_all_installed_apps_id()
)
try:
with open(os.path.join(APPS_SETTING_PATH, app_id, "settings.yml")) as f:
- settings = yaml.load(f)
+ settings = yaml.safe_load(f)
# If label contains unicode char, this may later trigger issues when building strings...
# FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think...
settings = {k: v for k, v in settings.items()}
@@ -2243,67 +1810,6 @@ def _set_app_settings(app_id, settings):
yaml.safe_dump(settings, f, default_flow_style=False)
-def _extract_app_from_file(path, remove=False):
- """
- Unzip or untar application tarball in APP_TMP_FOLDER, or copy it from a directory
-
- Keyword arguments:
- path -- Path of the tarball or directory
- remove -- Remove the tarball after extraction
-
- Returns:
- Dict manifest
-
- """
- logger.debug(m18n.n("extracting"))
-
- if os.path.exists(APP_TMP_FOLDER):
- shutil.rmtree(APP_TMP_FOLDER)
- os.makedirs(APP_TMP_FOLDER)
-
- path = os.path.abspath(path)
-
- if ".zip" in path:
- extract_result = os.system(
- "unzip %s -d %s > /dev/null 2>&1" % (path, APP_TMP_FOLDER)
- )
- if remove:
- os.remove(path)
- elif ".tar" in path:
- extract_result = os.system(
- "tar -xf %s -C %s > /dev/null 2>&1" % (path, APP_TMP_FOLDER)
- )
- if remove:
- os.remove(path)
- elif os.path.isdir(path):
- shutil.rmtree(APP_TMP_FOLDER)
- if path[-1] != "/":
- path = path + "/"
- extract_result = os.system('cp -a "%s" %s' % (path, APP_TMP_FOLDER))
- else:
- extract_result = 1
-
- if extract_result != 0:
- raise YunohostError("app_extraction_failed")
-
- try:
- extracted_app_folder = APP_TMP_FOLDER
- if len(os.listdir(extracted_app_folder)) == 1:
- for folder in os.listdir(extracted_app_folder):
- extracted_app_folder = extracted_app_folder + "/" + folder
- manifest = _get_manifest_of_app(extracted_app_folder)
- manifest["lastUpdate"] = int(time.time())
- except IOError:
- raise YunohostError("app_install_files_invalid")
- except ValueError as e:
- raise YunohostError("app_manifest_invalid", error=e)
-
- logger.debug(m18n.n("done"))
-
- manifest["remote"] = {"type": "file", "path": path}
- return manifest, extracted_app_folder
-
-
def _get_manifest_of_app(path):
"Get app manifest stored in json or in toml"
@@ -2489,155 +1995,203 @@ def _set_default_ask_questions(arguments):
key = "app_manifest_%s_ask_%s" % (script_name, arg["name"])
arg["ask"] = m18n.n(key)
+ # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value...
+ if arg.get("type") in ["domain", "user", "password"]:
+ if "example" in arg:
+ del arg["example"]
+ if "default" in arg:
+ del arg["domain"]
+
return arguments
-def _get_git_last_commit_hash(repository, reference="HEAD"):
- """
- Attempt to retrieve the last commit hash of a git repository
+def _is_app_repo_url(string: str) -> bool:
- Keyword arguments:
- repository -- The URL or path of the repository
+ string = string.strip()
+ # Dummy test for ssh-based stuff ... should probably be improved somehow
+ if "@" in string:
+ return True
+
+ return bool(APP_REPO_URL.match(string))
+
+
+def _app_quality(src: str) -> str:
"""
- try:
- cmd = "git ls-remote --exit-code {0} {1} | awk '{{print $1}}'".format(
- repository, reference
- )
- commit = check_output(cmd)
- except subprocess.CalledProcessError:
- logger.error("unable to get last commit from %s", repository)
- raise ValueError("Unable to get last commit with git")
+ app may in fact be an app name, an url, or a path
+ """
+
+ raw_app_catalog = _load_apps_catalog()["apps"]
+ if src in raw_app_catalog or _is_app_repo_url(src):
+
+ # If we got an app name directly (e.g. just "wordpress"), we gonna test this name
+ if src in raw_app_catalog:
+ app_name_to_test = src
+ # If we got an url like "https://github.com/foo/bar_ynh, we want to
+ # extract "bar" and test if we know this app
+ elif ("http://" in src) or ("https://" in src):
+ app_name_to_test = src.strip("/").split("/")[-1].replace("_ynh", "")
+ else:
+ # FIXME : watdo if '@' in app ?
+ return "thirdparty"
+
+ if app_name_to_test in raw_app_catalog:
+
+ state = raw_app_catalog[app_name_to_test].get("state", "notworking")
+ level = raw_app_catalog[app_name_to_test].get("level", None)
+ if state in ["working", "validated"]:
+ if isinstance(level, int) and level >= 5:
+ return "success"
+ elif isinstance(level, int) and level > 0:
+ return "warning"
+ return "danger"
+ else:
+ return "thirdparty"
+
+ elif os.path.exists(src):
+ return "thirdparty"
else:
- return commit.strip()
+ if "http://" in src or "https://" in src:
+ logger.error(
+ f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh"
+ )
+ raise YunohostValidationError("app_unknown")
-def _fetch_app_from_git(app):
+def _extract_app(src: str) -> Tuple[Dict, str]:
"""
- Unzip or untar application tarball in APP_TMP_FOLDER
-
- Keyword arguments:
- app -- App_id or git repo URL
-
- Returns:
- Dict manifest
-
+ src may be an app name, an url, or a path
"""
- extracted_app_folder = APP_TMP_FOLDER
- app_tmp_archive = "{0}.zip".format(extracted_app_folder)
- if os.path.exists(extracted_app_folder):
- shutil.rmtree(extracted_app_folder)
- if os.path.exists(app_tmp_archive):
- os.remove(app_tmp_archive)
+ raw_app_catalog = _load_apps_catalog()["apps"]
- logger.debug(m18n.n("downloading"))
+ # App is an appname in the catalog
+ if src in raw_app_catalog:
+ if "git" not in raw_app_catalog[src]:
+ raise YunohostValidationError("app_unsupported_remote_type")
- # Extract URL, branch and revision to download
- if ("@" in app) or ("http://" in app) or ("https://" in app):
- url = app
- branch = "master"
- if "/tree/" in url:
- url, branch = url.split("/tree/", 1)
- revision = "HEAD"
- else:
- app_dict = _load_apps_catalog()["apps"]
-
- app_id, _ = _parse_app_instance_name(app)
-
- if app_id not in app_dict:
- raise YunohostError("app_unknown")
- elif "git" not in app_dict[app_id]:
- raise YunohostError("app_unsupported_remote_type")
-
- app_info = app_dict[app_id]
+ app_info = raw_app_catalog[src]
url = app_info["git"]["url"]
branch = app_info["git"]["branch"]
revision = str(app_info["git"]["revision"])
+ return _extract_app_from_gitrepo(url, branch, revision, app_info)
+ # App is a git repo url
+ elif _is_app_repo_url(src):
+ url = src.strip().strip("/")
+ branch = "master"
+ revision = "HEAD"
+ # gitlab urls may look like 'https://domain/org/group/repo/-/tree/testing'
+ # compated to github urls looking like 'https://domain/org/repo/tree/testing'
+ if "/-/" in url:
+ url = url.replace("/-/", "/")
+ if "/tree/" in url:
+ url, branch = url.split("/tree/", 1)
+ return _extract_app_from_gitrepo(url, branch, revision, {})
+ # App is a local folder
+ elif os.path.exists(src):
+ return _extract_app_from_folder(src)
+ else:
+ if "http://" in src or "https://" in src:
+ logger.error(
+ f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh"
+ )
+ raise YunohostValidationError("app_unknown")
+
+
+def _extract_app_from_folder(path: str) -> Tuple[Dict, str]:
+ """
+ Unzip / untar / copy application tarball or directory to a tmp work directory
+
+ Keyword arguments:
+ path -- Path of the tarball or directory
+ """
+ logger.debug(m18n.n("extracting"))
+
+ path = os.path.abspath(path)
+
+ extracted_app_folder = _make_tmp_workdir_for_app()
+
+ if os.path.isdir(path):
+ shutil.rmtree(extracted_app_folder)
+ if path[-1] != "/":
+ path = path + "/"
+ cp(path, extracted_app_folder, recursive=True)
+ else:
+ try:
+ shutil.unpack_archive(path, extracted_app_folder)
+ except Exception:
+ raise YunohostError("app_extraction_failed")
+
+ try:
+ if len(os.listdir(extracted_app_folder)) == 1:
+ for folder in os.listdir(extracted_app_folder):
+ extracted_app_folder = extracted_app_folder + "/" + folder
+ except IOError:
+ raise YunohostError("app_install_files_invalid")
+
+ manifest = _get_manifest_of_app(extracted_app_folder)
+ manifest["lastUpdate"] = int(time.time())
+
+ logger.debug(m18n.n("done"))
+
+ manifest["remote"] = {"type": "file", "path": path}
+ return manifest, extracted_app_folder
+
+
+def _extract_app_from_gitrepo(
+ url: str, branch: str, revision: str, app_info: Dict = {}
+) -> Tuple[Dict, str]:
+
+ logger.debug(m18n.n("downloading"))
+
+ extracted_app_folder = _make_tmp_workdir_for_app()
# Download only this commit
try:
# We don't use git clone because, git clone can't download
# a specific revision only
+ ref = branch if revision == "HEAD" else revision
run_commands([["git", "init", extracted_app_folder]], shell=False)
run_commands(
[
["git", "remote", "add", "origin", url],
- [
- "git",
- "fetch",
- "--depth=1",
- "origin",
- branch if revision == "HEAD" else revision,
- ],
+ ["git", "fetch", "--depth=1", "origin", ref],
["git", "reset", "--hard", "FETCH_HEAD"],
],
cwd=extracted_app_folder,
shell=False,
)
- manifest = _get_manifest_of_app(extracted_app_folder)
except subprocess.CalledProcessError:
raise YunohostError("app_sources_fetch_failed")
- except ValueError as e:
- raise YunohostError("app_manifest_invalid", error=e)
else:
logger.debug(m18n.n("done"))
+ manifest = _get_manifest_of_app(extracted_app_folder)
+
# Store remote repository info into the returned manifest
manifest["remote"] = {"type": "git", "url": url, "branch": branch}
if revision == "HEAD":
try:
- manifest["remote"]["revision"] = _get_git_last_commit_hash(url, branch)
+ # Get git last commit hash
+ cmd = f"git ls-remote --exit-code {url} {branch} | awk '{{print $1}}'"
+ manifest["remote"]["revision"] = check_output(cmd)
except Exception as e:
- logger.debug("cannot get last commit hash because: %s ", e)
+ logger.warning("cannot get last commit hash because: %s ", e)
else:
manifest["remote"]["revision"] = revision
- manifest["lastUpdate"] = app_info["lastUpdate"]
+ manifest["lastUpdate"] = app_info.get("lastUpdate")
return manifest, extracted_app_folder
-def _installed_instance_number(app, last=False):
- """
- Check if application is installed and return instance number
-
- Keyword arguments:
- app -- id of App to check
- last -- Return only last instance number
-
- Returns:
- Number of last installed instance | List or instances
-
- """
- if last:
- number = 0
- try:
- installed_apps = os.listdir(APPS_SETTING_PATH)
- except OSError:
- os.makedirs(APPS_SETTING_PATH)
- return 0
-
- for installed_app in installed_apps:
- if number == 0 and app == installed_app:
- number = 1
- elif "__" in installed_app:
- if app == installed_app[: installed_app.index("__")]:
- if int(installed_app[installed_app.index("__") + 2 :]) > number:
- number = int(installed_app[installed_app.index("__") + 2 :])
-
- return number
-
- else:
- instance_number_list = []
- instances_dict = app_map(app=app, raw=True)
- for key, domain in instances_dict.items():
- for key, path in domain.items():
- instance_number_list.append(path["instance"])
-
- return sorted(instance_number_list)
+#
+# ############################### #
+# Small utilities #
+# ############################### #
+#
-def _is_installed(app):
+def _is_installed(app: str) -> bool:
"""
Check if application is installed
@@ -2651,421 +2205,83 @@ def _is_installed(app):
return os.path.isdir(APPS_SETTING_PATH + app)
-def _installed_apps():
+def _assert_is_installed(app: str) -> None:
+ if not _is_installed(app):
+ raise YunohostValidationError(
+ "app_not_installed", app=app, all_apps=_get_all_installed_apps_id()
+ )
+
+
+def _installed_apps() -> List[str]:
return os.listdir(APPS_SETTING_PATH)
-def _value_for_locale(values):
+def _get_all_installed_apps_id():
"""
- Return proper value for current locale
-
- Keyword arguments:
- values -- A dict of values associated to their locale
-
- Returns:
- An utf-8 encoded string
-
+ Return something like:
+ ' * app1
+ * app2
+ * ...'
"""
- if not isinstance(values, dict):
- return values
- for lang in [m18n.locale, m18n.default_locale]:
- try:
- return values[lang]
- except KeyError:
- continue
+ all_apps_ids = sorted(_installed_apps())
- # Fallback to first value
- return list(values.values())[0]
+ all_apps_ids_formatted = "\n * ".join(all_apps_ids)
+ all_apps_ids_formatted = "\n * " + all_apps_ids_formatted
+
+ return all_apps_ids_formatted
-def _check_manifest_requirements(manifest, app_instance_name):
+def _check_manifest_requirements(manifest: Dict):
"""Check if required packages are met from the manifest"""
packaging_format = int(manifest.get("packaging_format", 0))
if packaging_format not in [0, 1]:
- raise YunohostError("app_packaging_format_not_supported")
+ raise YunohostValidationError("app_packaging_format_not_supported")
requirements = manifest.get("requirements", dict())
if not requirements:
return
- logger.debug(m18n.n("app_requirements_checking", app=app_instance_name))
+ app = manifest.get("id", "?")
+
+ logger.debug(m18n.n("app_requirements_checking", app=app))
# Iterate over requirements
for pkgname, spec in requirements.items():
if not packages.meets_version_specifier(pkgname, spec):
version = packages.ynh_packages_version()[pkgname]["version"]
- raise YunohostError(
+ raise YunohostValidationError(
"app_requirements_unmeet",
pkgname=pkgname,
version=version,
spec=spec,
- app=app_instance_name,
+ app=app,
)
-def _parse_args_from_manifest(manifest, action, args={}):
- """Parse arguments needed for an action from the manifest
-
- Retrieve specified arguments for the action from the manifest, and parse
- given args according to that. If some required arguments are not provided,
- its values will be asked if interaction is possible.
- Parsed arguments will be returned as an OrderedDict
-
- Keyword arguments:
- manifest -- The app manifest to use
- action -- The action to retrieve arguments for
- args -- A dictionnary of arguments to parse
-
- """
- if action not in manifest["arguments"]:
- logger.debug("no arguments found for '%s' in manifest", action)
- return OrderedDict()
-
- action_args = manifest["arguments"][action]
- return _parse_args_in_yunohost_format(args, action_args)
-
-
-def _parse_args_for_action(action, args={}):
- """Parse arguments needed for an action from the actions list
-
- Retrieve specified arguments for the action from the manifest, and parse
- given args according to that. If some required arguments are not provided,
- its values will be asked if interaction is possible.
- Parsed arguments will be returned as an OrderedDict
-
- Keyword arguments:
- action -- The action
- args -- A dictionnary of arguments to parse
-
- """
- args_dict = OrderedDict()
-
- if "arguments" not in action:
- logger.debug("no arguments found for '%s' in manifest", action)
- return args_dict
-
- action_args = action["arguments"]
-
- return _parse_args_in_yunohost_format(args, action_args)
-
-
-class Question:
- "empty class to store questions information"
-
-
-class YunoHostArgumentFormatParser(object):
- hide_user_input_in_prompt = False
-
- def parse_question(self, question, user_answers):
- parsed_question = Question()
-
- parsed_question.name = question["name"]
- parsed_question.default = question.get("default", None)
- parsed_question.choices = question.get("choices", [])
- parsed_question.optional = question.get("optional", False)
- parsed_question.ask = question.get("ask")
- parsed_question.value = user_answers.get(parsed_question.name)
-
- if parsed_question.ask is None:
- parsed_question.ask = "Enter value for '%s':" % parsed_question.name
-
- # Empty value is parsed as empty string
- if parsed_question.default == "":
- parsed_question.default = None
-
- return parsed_question
-
- def parse(self, question, user_answers):
- question = self.parse_question(question, user_answers)
-
- if question.value is None:
- text_for_user_input_in_cli = self._format_text_for_user_input_in_cli(
- question
- )
-
- try:
- question.value = msignals.prompt(
- text_for_user_input_in_cli, self.hide_user_input_in_prompt
- )
- except NotImplementedError:
- question.value = None
-
- # we don't have an answer, check optional and default_value
- if question.value is None or question.value == "":
- if not question.optional and question.default is None:
- raise YunohostError("app_argument_required", name=question.name)
- else:
- question.value = (
- getattr(self, "default_value", None)
- if question.default is None
- else question.default
- )
-
- # we have an answer, do some post checks
- if question.value is not None:
- if question.choices and question.value not in question.choices:
- self._raise_invalid_answer(question)
-
- # this is done to enforce a certain formating like for boolean
- # by default it doesn't do anything
- question.value = self._post_parse_value(question)
-
- return (question.value, self.argument_type)
-
- def _raise_invalid_answer(self, question):
- raise YunohostError(
- "app_argument_choice_invalid",
- name=question.name,
- choices=", ".join(question.choices),
- )
-
- def _format_text_for_user_input_in_cli(self, question):
- text_for_user_input_in_cli = _value_for_locale(question.ask)
-
- if question.choices:
- text_for_user_input_in_cli += " [{0}]".format(" | ".join(question.choices))
-
- if question.default is not None:
- text_for_user_input_in_cli += " (default: {0})".format(question.default)
-
- return text_for_user_input_in_cli
-
- def _post_parse_value(self, question):
- return question.value
-
-
-class StringArgumentParser(YunoHostArgumentFormatParser):
- argument_type = "string"
- default_value = ""
-
-
-class PasswordArgumentParser(YunoHostArgumentFormatParser):
- hide_user_input_in_prompt = True
- argument_type = "password"
- default_value = ""
- forbidden_chars = "{}"
-
- def parse_question(self, question, user_answers):
- question = super(PasswordArgumentParser, self).parse_question(
- question, user_answers
- )
-
- if question.default is not None:
- raise YunohostError("app_argument_password_no_default", name=question.name)
-
- return question
-
- def _post_parse_value(self, question):
- if any(char in question.value for char in self.forbidden_chars):
- raise YunohostError(
- "pattern_password_app", forbidden_chars=self.forbidden_chars
- )
-
- # If it's an optional argument the value should be empty or strong enough
- if not question.optional or question.value:
- from yunohost.utils.password import assert_password_is_strong_enough
-
- assert_password_is_strong_enough("user", question.value)
-
- return super(PasswordArgumentParser, self)._post_parse_value(question)
-
-
-class PathArgumentParser(YunoHostArgumentFormatParser):
- argument_type = "path"
- default_value = ""
-
-
-class BooleanArgumentParser(YunoHostArgumentFormatParser):
- argument_type = "boolean"
- default_value = False
-
- def parse_question(self, question, user_answers):
- question = super(BooleanArgumentParser, self).parse_question(
- question, user_answers
- )
-
- if question.default is None:
- question.default = False
-
- return question
-
- def _format_text_for_user_input_in_cli(self, question):
- text_for_user_input_in_cli = _value_for_locale(question.ask)
-
- text_for_user_input_in_cli += " [yes | no]"
-
- if question.default is not None:
- formatted_default = "yes" if question.default else "no"
- text_for_user_input_in_cli += " (default: {0})".format(formatted_default)
-
- return text_for_user_input_in_cli
-
- def _post_parse_value(self, question):
- if isinstance(question.value, bool):
- return 1 if question.value else 0
-
- if str(question.value).lower() in ["1", "yes", "y", "true"]:
- return 1
-
- if str(question.value).lower() in ["0", "no", "n", "false"]:
- return 0
-
- raise YunohostError(
- "app_argument_choice_invalid",
- name=question.name,
- choices="yes, no, y, n, 1, 0",
- )
-
-
-class DomainArgumentParser(YunoHostArgumentFormatParser):
- argument_type = "domain"
-
- def parse_question(self, question, user_answers):
- from yunohost.domain import domain_list, _get_maindomain
-
- question = super(DomainArgumentParser, self).parse_question(
- question, user_answers
- )
-
- if question.default is None:
- question.default = _get_maindomain()
-
- question.choices = domain_list()["domains"]
-
- return question
-
- def _raise_invalid_answer(self, question):
- raise YunohostError(
- "app_argument_invalid", name=question.name, error=m18n.n("domain_unknown")
- )
-
-
-class UserArgumentParser(YunoHostArgumentFormatParser):
- argument_type = "user"
-
- def parse_question(self, question, user_answers):
- from yunohost.user import user_list, user_info
- from yunohost.domain import _get_maindomain
-
- question = super(UserArgumentParser, self).parse_question(
- question, user_answers
- )
- question.choices = user_list()["users"]
- if question.default is None:
- root_mail = "root@%s" % _get_maindomain()
- for user in question.choices.keys():
- if root_mail in user_info(user).get("mail-aliases", []):
- question.default = user
- break
-
- return question
-
- def _raise_invalid_answer(self, question):
- raise YunohostError(
- "app_argument_invalid",
- name=question.name,
- error=m18n.n("user_unknown", user=question.value),
- )
-
-
-class NumberArgumentParser(YunoHostArgumentFormatParser):
- argument_type = "number"
- default_value = ""
-
- def parse_question(self, question, user_answers):
- question = super(NumberArgumentParser, self).parse_question(
- question, user_answers
- )
-
- if question.default is None:
- question.default = 0
-
- return question
-
- def _post_parse_value(self, question):
- if isinstance(question.value, int):
- return super(NumberArgumentParser, self)._post_parse_value(question)
-
- if isinstance(question.value, str) and question.value.isdigit():
- return int(question.value)
-
- raise YunohostError(
- "app_argument_invalid", name=question.name, error=m18n.n("invalid_number")
- )
-
-
-class DisplayTextArgumentParser(YunoHostArgumentFormatParser):
- argument_type = "display_text"
-
- def parse(self, question, user_answers):
- print(question["ask"])
-
-
-ARGUMENTS_TYPE_PARSERS = {
- "string": StringArgumentParser,
- "password": PasswordArgumentParser,
- "path": PathArgumentParser,
- "boolean": BooleanArgumentParser,
- "domain": DomainArgumentParser,
- "user": UserArgumentParser,
- "number": NumberArgumentParser,
- "display_text": DisplayTextArgumentParser,
-}
-
-
-def _parse_args_in_yunohost_format(user_answers, argument_questions):
- """Parse arguments store in either manifest.json or actions.json or from a
- config panel against the user answers when they are present.
-
- Keyword arguments:
- user_answers -- a dictionnary of arguments from the user (generally
- empty in CLI, filed from the admin interface)
- argument_questions -- the arguments description store in yunohost
- format from actions.json/toml, manifest.json/toml
- or config_panel.json/toml
- """
- parsed_answers_dict = OrderedDict()
-
- for question in argument_questions:
- parser = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")]()
-
- answer = parser.parse(question=question, user_answers=user_answers)
- if answer is not None:
- parsed_answers_dict[question["name"]] = answer
-
- return parsed_answers_dict
-
-
-def _validate_and_normalize_webpath(args_dict, app_folder):
+def _guess_webapp_path_requirement(app_folder: str) -> str:
# If there's only one "domain" and "path", validate that domain/path
# is an available url and normalize the path.
- domain_args = [
- (name, value[0]) for name, value in args_dict.items() if value[1] == "domain"
+ manifest = _get_manifest_of_app(app_folder)
+ raw_questions = manifest.get("arguments", {}).get("install", {})
+
+ domain_questions = [
+ question for question in raw_questions if question.get("type") == "domain"
]
- path_args = [
- (name, value[0]) for name, value in args_dict.items() if value[1] == "path"
+ path_questions = [
+ question for question in raw_questions if question.get("type") == "path"
]
- if len(domain_args) == 1 and len(path_args) == 1:
-
- domain = domain_args[0][1]
- path = path_args[0][1]
- domain, path = _normalize_domain_path(domain, path)
-
- # Check the url is available
- _assert_no_conflicting_apps(domain, path)
-
- # (We save this normalized path so that the install script have a
- # standard path format to deal with no matter what the user inputted)
- args_dict[path_args[0][0]] = (path, "path")
-
- # This is likely to be a full-domain app...
- elif len(domain_args) == 1 and len(path_args) == 0:
+ if len(domain_questions) == 0 and len(path_questions) == 0:
+ return ""
+ if len(domain_questions) == 1 and len(path_questions) == 1:
+ return "domain_and_path"
+ if len(domain_questions) == 1 and len(path_questions) == 0:
+ # This is likely to be a full-domain app...
# Confirm that this is a full-domain app This should cover most cases
# ... though anyway the proper solution is to implement some mechanism
@@ -3075,36 +2291,30 @@ def _validate_and_normalize_webpath(args_dict, app_folder):
# Full-domain apps typically declare something like path_url="/" or path=/
# and use ynh_webpath_register or yunohost_app_checkurl inside the install script
- install_script_content = open(
- os.path.join(app_folder, "scripts/install")
- ).read()
+ install_script_content = read_file(os.path.join(app_folder, "scripts/install"))
if re.search(
- r"\npath(_url)?=[\"']?/[\"']?\n", install_script_content
- ) and re.search(
- r"(ynh_webpath_register|yunohost app checkurl)", install_script_content
- ):
+ r"\npath(_url)?=[\"']?/[\"']?", install_script_content
+ ) and re.search(r"ynh_webpath_register", install_script_content):
+ return "full_domain"
- domain = domain_args[0][1]
- _assert_no_conflicting_apps(domain, "/", full_domain=True)
+ return "?"
-def _normalize_domain_path(domain, path):
+def _validate_webpath_requirement(
+ args: Dict[str, Any], path_requirement: str, ignore_app=None
+) -> None:
- # We want url to be of the format :
- # some.domain.tld/foo
+ domain = args.get("domain")
+ path = args.get("path")
- # Remove http/https prefix if it's there
- if domain.startswith("https://"):
- domain = domain[len("https://") :]
- elif domain.startswith("http://"):
- domain = domain[len("http://") :]
+ if path_requirement == "domain_and_path":
+ _assert_no_conflicting_apps(domain, path, ignore_app=ignore_app)
- # Remove trailing slashes
- domain = domain.rstrip("/").lower()
- path = "/" + path.strip("/")
-
- return domain, path
+ elif path_requirement == "full_domain":
+ _assert_no_conflicting_apps(
+ domain, "/", full_domain=True, ignore_app=ignore_app
+ )
def _get_conflicting_apps(domain, path, ignore_app=None):
@@ -3117,13 +2327,13 @@ def _get_conflicting_apps(domain, path, ignore_app=None):
ignore_app -- An optional app id to ignore (c.f. the change_url usecase)
"""
- from yunohost.domain import domain_list
+ from yunohost.domain import _assert_domain_exists
- domain, path = _normalize_domain_path(domain, path)
+ domain = DomainQuestion.normalize(domain)
+ path = PathQuestion.normalize(path)
# Abort if domain is unknown
- if domain not in domain_list()["domains"]:
- raise YunohostError("domain_name_unknown", domain=domain)
+ _assert_domain_exists(domain)
# Fetch apps map
apps_map = app_map(raw=True)
@@ -3162,12 +2372,16 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False
)
if full_domain:
- raise YunohostError("app_full_domain_unavailable", domain=domain)
+ raise YunohostValidationError("app_full_domain_unavailable", domain=domain)
else:
- raise YunohostError("app_location_unavailable", apps="\n".join(apps))
+ raise YunohostValidationError(
+ "app_location_unavailable", apps="\n".join(apps)
+ )
-def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"):
+def _make_environment_for_app_script(
+ app, args={}, args_prefix="APP_ARG_", workdir=None
+):
app_setting_path = os.path.join(APPS_SETTING_PATH, app)
@@ -3181,227 +2395,89 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"):
"YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"),
}
- for arg_name, arg_value_and_type in args.items():
- env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(
- arg_value_and_type[0]
- )
+ if workdir:
+ env_dict["YNH_APP_BASEDIR"] = workdir
+
+ for arg_name, arg_value in args.items():
+ env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(arg_value)
return env_dict
-def _parse_app_instance_name(app_instance_name):
+def _parse_app_instance_name(app_instance_name: str) -> Tuple[str, int]:
"""
Parse a Yunohost app instance name and extracts the original appid
and the application instance number
- >>> _parse_app_instance_name('yolo') == ('yolo', 1)
- True
- >>> _parse_app_instance_name('yolo1') == ('yolo1', 1)
- True
- >>> _parse_app_instance_name('yolo__0') == ('yolo__0', 1)
- True
- >>> _parse_app_instance_name('yolo__1') == ('yolo', 1)
- True
- >>> _parse_app_instance_name('yolo__23') == ('yolo', 23)
- True
- >>> _parse_app_instance_name('yolo__42__72') == ('yolo__42', 72)
- True
- >>> _parse_app_instance_name('yolo__23qdqsd') == ('yolo__23qdqsd', 1)
- True
- >>> _parse_app_instance_name('yolo__23qdqsd56') == ('yolo__23qdqsd56', 1)
- True
+ 'yolo' -> ('yolo', 1)
+ 'yolo1' -> ('yolo1', 1)
+ 'yolo__0' -> ('yolo__0', 1)
+ 'yolo__1' -> ('yolo', 1)
+ 'yolo__23' -> ('yolo', 23)
+ 'yolo__42__72' -> ('yolo__42', 72)
+ 'yolo__23qdqsd' -> ('yolo__23qdqsd', 1)
+ 'yolo__23qdqsd56' -> ('yolo__23qdqsd56', 1)
"""
match = re_app_instance_name.match(app_instance_name)
- assert match, "Could not parse app instance name : %s" % app_instance_name
+ assert match, f"Could not parse app instance name : {app_instance_name}"
appid = match.groupdict().get("appid")
- app_instance_nb = (
- int(match.groupdict().get("appinstancenb"))
- if match.groupdict().get("appinstancenb") is not None
- else 1
- )
+ app_instance_nb_ = match.groupdict().get("appinstancenb") or "1"
+ if not appid:
+ raise Exception(f"Could not parse app instance name : {app_instance_name}")
+ if not str(app_instance_nb_).isdigit():
+ raise Exception(f"Could not parse app instance name : {app_instance_name}")
+ else:
+ app_instance_nb = int(str(app_instance_nb_))
+
return (appid, app_instance_nb)
-#
-# ############################### #
-# Applications list management #
-# ############################### #
-#
+def _next_instance_number_for_app(app):
+
+ # Get list of sibling apps, such as {app}, {app}__2, {app}__4
+ apps = _installed_apps()
+ sibling_app_ids = [a for a in apps if a == app or a.startswith(f"{app}__")]
+
+ # Find the list of ids, such as [1, 2, 4]
+ sibling_ids = [_parse_app_instance_name(a)[1] for a in sibling_app_ids]
+
+ # Find the first 'i' that's not in the sibling_ids list already
+ i = 1
+ while True:
+ if i not in sibling_ids:
+ return i
+ else:
+ i += 1
-def _initialize_apps_catalog_system():
- """
- This function is meant to intialize the apps_catalog system with YunoHost's default app catalog.
+def _make_tmp_workdir_for_app(app=None):
- It also creates the cron job that will update the list every day
- """
+ # Create parent dir if it doesn't exists yet
+ if not os.path.exists(APP_TMP_WORKDIRS):
+ os.makedirs(APP_TMP_WORKDIRS)
- default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}]
+ now = int(time.time())
- cron_job = []
- cron_job.append("#!/bin/bash")
- # We add a random delay between 0 and 60 min to avoid every instance fetching
- # the apps catalog at the same time every night
- cron_job.append("(sleep $((RANDOM%3600));")
- cron_job.append("yunohost tools update --apps > /dev/null) &")
- try:
- logger.debug(
- "Initializing apps catalog system with YunoHost's default app list"
- )
- write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list)
+ # Cleanup old dirs (if any)
+ for dir_ in os.listdir(APP_TMP_WORKDIRS):
+ path = os.path.join(APP_TMP_WORKDIRS, dir_)
+ # We only delete folders older than an arbitary 12 hours
+ # This is to cover the stupid case of upgrades
+ # Where many app will call 'yunohost backup create'
+ # from the upgrade script itself,
+ # which will also call this function while the upgrade
+ # script itself is running in one of those dir...
+ # It could be that there are other edge cases
+ # such as app-install-during-app-install
+ if os.stat(path).st_mtime < now - 12 * 3600:
+ shutil.rmtree(path)
+ tmpdir = tempfile.mkdtemp(prefix="app_", dir=APP_TMP_WORKDIRS)
- logger.debug("Installing apps catalog fetch daily cron job")
- write_to_file(APPS_CATALOG_CRON_PATH, "\n".join(cron_job))
- chown(APPS_CATALOG_CRON_PATH, uid="root", gid="root")
- chmod(APPS_CATALOG_CRON_PATH, 0o755)
- except Exception as e:
- raise YunohostError(
- "Could not initialize the apps catalog system... : %s" % str(e)
- )
+ # Copy existing app scripts, conf, ... if an app arg was provided
+ if app:
+ os.system(f"cp -a {APPS_SETTING_PATH}/{app}/* {tmpdir}")
- logger.success(m18n.n("apps_catalog_init_success"))
-
-
-def _read_apps_catalog_list():
- """
- Read the json corresponding to the list of apps catalogs
- """
-
- try:
- list_ = read_yaml(APPS_CATALOG_CONF)
- # Support the case where file exists but is empty
- # by returning [] if list_ is None
- return list_ if list_ else []
- except Exception as e:
- raise YunohostError("Could not read the apps_catalog list ... : %s" % str(e))
-
-
-def _actual_apps_catalog_api_url(base_url):
-
- return "{base_url}/v{version}/apps.json".format(
- base_url=base_url, version=APPS_CATALOG_API_VERSION
- )
-
-
-def _update_apps_catalog():
- """
- Fetches the json for each apps_catalog and update the cache
-
- apps_catalog_list is for example :
- [ {"id": "default", "url": "https://app.yunohost.org/default/"} ]
-
- Then for each apps_catalog, the actual json URL to be fetched is like :
- https://app.yunohost.org/default/vX/apps.json
-
- And store it in :
- /var/cache/yunohost/repo/default.json
- """
-
- apps_catalog_list = _read_apps_catalog_list()
-
- logger.info(m18n.n("apps_catalog_updating"))
-
- # Create cache folder if needed
- if not os.path.exists(APPS_CATALOG_CACHE):
- logger.debug("Initialize folder for apps catalog cache")
- mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root")
-
- for apps_catalog in apps_catalog_list:
- apps_catalog_id = apps_catalog["id"]
- actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"])
-
- # Fetch the json
- try:
- apps_catalog_content = download_json(actual_api_url)
- except Exception as e:
- raise YunohostError(
- "apps_catalog_failed_to_download",
- apps_catalog=apps_catalog_id,
- error=str(e),
- )
-
- # Remember the apps_catalog api version for later
- apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION
-
- # Save the apps_catalog data in the cache
- cache_file = "{cache_folder}/{list}.json".format(
- cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id
- )
- try:
- write_to_json(cache_file, apps_catalog_content)
- except Exception as e:
- raise YunohostError(
- "Unable to write cache data for %s apps_catalog : %s"
- % (apps_catalog_id, str(e))
- )
-
- logger.success(m18n.n("apps_catalog_update_success"))
-
-
-def _load_apps_catalog():
- """
- Read all the apps catalog cache files and build a single dict (merged_catalog)
- corresponding to all known apps and categories
- """
-
- merged_catalog = {"apps": {}, "categories": []}
-
- for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]:
-
- # Let's load the json from cache for this catalog
- cache_file = "{cache_folder}/{list}.json".format(
- cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id
- )
-
- try:
- apps_catalog_content = (
- read_json(cache_file) if os.path.exists(cache_file) else None
- )
- except Exception as e:
- raise YunohostError(
- "Unable to read cache for apps_catalog %s : %s" % (cache_file, e),
- raw_msg=True,
- )
-
- # Check that the version of the data matches version ....
- # ... otherwise it means we updated yunohost in the meantime
- # and need to update the cache for everything to be consistent
- if (
- not apps_catalog_content
- or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION
- ):
- logger.info(m18n.n("apps_catalog_obsolete_cache"))
- _update_apps_catalog()
- apps_catalog_content = read_json(cache_file)
-
- del apps_catalog_content["from_api_version"]
-
- # Add apps from this catalog to the output
- for app, info in apps_catalog_content["apps"].items():
-
- # (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ...
- # in which case we keep only the first one found)
- if app in merged_catalog["apps"]:
- logger.warning(
- "Duplicate app %s found between apps catalog %s and %s"
- % (app, apps_catalog_id, merged_catalog["apps"][app]["repository"])
- )
- continue
-
- info["repository"] = apps_catalog_id
- merged_catalog["apps"][app] = info
-
- # Annnnd categories
- merged_catalog["categories"] += apps_catalog_content["categories"]
-
- return merged_catalog
-
-
-#
-# ############################### #
-# Small utilities #
-# ############################### #
-#
+ return tmpdir
def is_true(arg):
@@ -3441,6 +2517,8 @@ def unstable_apps():
def _assert_system_is_sane_for_app(manifest, when):
+ from yunohost.service import service_status
+
logger.debug("Checking that required services are up and running...")
services = manifest.get("services", [])
@@ -3465,11 +2543,25 @@ def _assert_system_is_sane_for_app(manifest, when):
if "fail2ban" not in services:
services.append("fail2ban")
+ # Wait if a service is reloading
+ test_nb = 0
+ while test_nb < 16:
+ if not any(s for s in services if service_status(s)["status"] == "reloading"):
+ break
+ time.sleep(0.5)
+ test_nb += 1
+
# List services currently down and raise an exception if any are found
- faulty_services = [s for s in services if service_status(s)["status"] != "running"]
+ services_status = {s: service_status(s) for s in services}
+ faulty_services = [
+ f"{s} ({status['status']})"
+ for s, status in services_status.items()
+ if status["status"] != "running"
+ ]
+
if faulty_services:
if when == "pre":
- raise YunohostError(
+ raise YunohostValidationError(
"app_action_cannot_be_ran_because_required_services_down",
services=", ".join(faulty_services),
)
@@ -3480,212 +2572,6 @@ def _assert_system_is_sane_for_app(manifest, when):
if packages.dpkg_is_broken():
if when == "pre":
- raise YunohostError("dpkg_is_broken")
+ raise YunohostValidationError("dpkg_is_broken")
elif when == "post":
raise YunohostError("this_action_broke_dpkg")
-
-
-LEGACY_PHP_VERSION_REPLACEMENTS = [
- ("/etc/php5", "/etc/php/7.3"),
- ("/etc/php/7.0", "/etc/php/7.3"),
- ("/var/run/php5-fpm", "/var/run/php/php7.3-fpm"),
- ("/var/run/php/php7.0-fpm", "/var/run/php/php7.3-fpm"),
- ("php5", "php7.3"),
- ("php7.0", "php7.3"),
- (
- 'phpversion="${phpversion:-7.0}"',
- 'phpversion="${phpversion:-7.3}"',
- ), # Many helpers like the composer ones use 7.0 by default ...
- (
- '"$phpversion" == "7.0"',
- '$(bc <<< "$phpversion >= 7.3") -eq 1',
- ), # patch ynh_install_php to refuse installing/removing php <= 7.3
-]
-
-
-def _patch_legacy_php_versions(app_folder):
-
- files_to_patch = []
- files_to_patch.extend(glob.glob("%s/conf/*" % app_folder))
- files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder))
- files_to_patch.extend(glob.glob("%s/scripts/*/*" % app_folder))
- files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder))
- files_to_patch.append("%s/manifest.json" % app_folder)
- files_to_patch.append("%s/manifest.toml" % app_folder)
-
- for filename in files_to_patch:
-
- # Ignore non-regular files
- if not os.path.isfile(filename):
- continue
-
- c = (
- "sed -i "
- + "".join(
- "-e 's@{pattern}@{replace}@g' ".format(pattern=p, replace=r)
- for p, r in LEGACY_PHP_VERSION_REPLACEMENTS
- )
- + "%s" % filename
- )
- os.system(c)
-
-
-def _patch_legacy_php_versions_in_settings(app_folder):
-
- settings = read_yaml(os.path.join(app_folder, "settings.yml"))
-
- if settings.get("fpm_config_dir") == "/etc/php/7.0/fpm":
- settings["fpm_config_dir"] = "/etc/php/7.3/fpm"
- if settings.get("fpm_service") == "php7.0-fpm":
- settings["fpm_service"] = "php7.3-fpm"
- if settings.get("phpversion") == "7.0":
- settings["phpversion"] = "7.3"
-
- # We delete these checksums otherwise the file will appear as manually modified
- list_to_remove = ["checksum__etc_php_7.0_fpm_pool", "checksum__etc_nginx_conf.d"]
- settings = {
- k: v
- for k, v in settings.items()
- if not any(k.startswith(to_remove) for to_remove in list_to_remove)
- }
-
- write_to_yaml(app_folder + "/settings.yml", settings)
-
-
-def _patch_legacy_helpers(app_folder):
-
- files_to_patch = []
- files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder))
- files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder))
-
- stuff_to_replace = {
- # Replace
- # sudo yunohost app initdb $db_user -p $db_pwd
- # by
- # ynh_mysql_setup_db --db_user=$db_user --db_name=$db_user --db_pwd=$db_pwd
- "yunohost app initdb": {
- "pattern": r"(sudo )?yunohost app initdb \"?(\$\{?\w+\}?)\"?\s+-p\s\"?(\$\{?\w+\}?)\"?",
- "replace": r"ynh_mysql_setup_db --db_user=\2 --db_name=\2 --db_pwd=\3",
- "important": True,
- },
- # Replace
- # sudo yunohost app checkport whaterver
- # by
- # ynh_port_available whatever
- "yunohost app checkport": {
- "pattern": r"(sudo )?yunohost app checkport",
- "replace": r"ynh_port_available",
- "important": True,
- },
- # We can't migrate easily port-available
- # .. but at the time of writing this code, only two non-working apps are using it.
- "yunohost tools port-available": {"important": True},
- # Replace
- # yunohost app checkurl "${domain}${path_url}" -a "${app}"
- # by
- # ynh_webpath_register --app=${app} --domain=${domain} --path_url=${path_url}
- "yunohost app checkurl": {
- "pattern": r"(sudo )?yunohost app checkurl \"?(\$\{?\w+\}?)\/?(\$\{?\w+\}?)\"?\s+-a\s\"?(\$\{?\w+\}?)\"?",
- "replace": r"ynh_webpath_register --app=\4 --domain=\2 --path_url=\3",
- "important": True,
- },
- # Remove
- # Automatic diagnosis data from YunoHost
- # __PRE_TAG1__$(yunohost tools diagnosis | ...)__PRE_TAG2__"
- #
- "yunohost tools diagnosis": {
- "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?",
- "replace": r"",
- "important": False,
- },
- # Old $1, $2 in backup/restore scripts...
- "app=$2": {
- "only_for": ["scripts/backup", "scripts/restore"],
- "pattern": r"app=\$2",
- "replace": r"app=$YNH_APP_INSTANCE_NAME",
- "important": True,
- },
- # Old $1, $2 in backup/restore scripts...
- "backup_dir=$1": {
- "only_for": ["scripts/backup", "scripts/restore"],
- "pattern": r"backup_dir=\$1",
- "replace": r"backup_dir=.",
- "important": True,
- },
- # Old $1, $2 in backup/restore scripts...
- "restore_dir=$1": {
- "only_for": ["scripts/restore"],
- "pattern": r"restore_dir=\$1",
- "replace": r"restore_dir=.",
- "important": True,
- },
- # Old $1, $2 in install scripts...
- # We ain't patching that shit because it ain't trivial to patch all args...
- "domain=$1": {"only_for": ["scripts/install"], "important": True},
- }
-
- for helper, infos in stuff_to_replace.items():
- infos["pattern"] = (
- re.compile(infos["pattern"]) if infos.get("pattern") else None
- )
- infos["replace"] = infos.get("replace")
-
- for filename in files_to_patch:
-
- # Ignore non-regular files
- if not os.path.isfile(filename):
- continue
-
- content = read_file(filename)
- replaced_stuff = False
- show_warning = False
-
- for helper, infos in stuff_to_replace.items():
-
- # Ignore if not relevant for this file
- if infos.get("only_for") and not any(
- filename.endswith(f) for f in infos["only_for"]
- ):
- continue
-
- # If helper is used, attempt to patch the file
- if helper in content and infos["pattern"]:
- content = infos["pattern"].sub(infos["replace"], content)
- replaced_stuff = True
- if infos["important"]:
- show_warning = True
-
- # If the helper is *still* in the content, it means that we
- # couldn't patch the deprecated helper in the previous lines. In
- # that case, abort the install or whichever step is performed
- if helper in content and infos["important"]:
- raise YunohostError(
- "This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.",
- raw_msg=True,
- )
-
- if replaced_stuff:
-
- # Check the app do load the helper
- # If it doesn't, add the instruction ourselve (making sure it's after the #!/bin/bash if it's there...
- if filename.split("/")[-1] in [
- "install",
- "remove",
- "upgrade",
- "backup",
- "restore",
- ]:
- source_helpers = "source /usr/share/yunohost/helpers"
- if source_helpers not in content:
- content.replace("#!/bin/bash", "#!/bin/bash\n" + source_helpers)
- if source_helpers not in content:
- content = source_helpers + "\n" + content
-
- # Actually write the new content in the file
- write_to_file(filename, content)
-
- if show_warning:
- # And complain about those damn deprecated helpers
- logger.error(
- r"/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ..."
- )
diff --git a/src/yunohost/app_catalog.py b/src/yunohost/app_catalog.py
new file mode 100644
index 000000000..e4ffa1db6
--- /dev/null
+++ b/src/yunohost/app_catalog.py
@@ -0,0 +1,255 @@
+import os
+import re
+
+from moulinette import m18n
+from moulinette.utils.log import getActionLogger
+from moulinette.utils.network import download_json
+from moulinette.utils.filesystem import (
+ read_json,
+ read_yaml,
+ write_to_json,
+ write_to_yaml,
+ mkdir,
+)
+
+from yunohost.utils.i18n import _value_for_locale
+from yunohost.utils.error import YunohostError
+
+logger = getActionLogger("yunohost.app_catalog")
+
+APPS_CATALOG_CACHE = "/var/cache/yunohost/repo"
+APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml"
+APPS_CATALOG_API_VERSION = 2
+APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default"
+
+
+# Old legacy function...
+def app_fetchlist():
+ logger.warning(
+ "'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead"
+ )
+ from yunohost.tools import tools_update
+
+ tools_update(target="apps")
+
+
+def app_catalog(full=False, with_categories=False):
+ """
+ Return a dict of apps available to installation from Yunohost's app catalog
+ """
+
+ from yunohost.app import _installed_apps, _set_default_ask_questions
+
+ # Get app list from catalog cache
+ catalog = _load_apps_catalog()
+ installed_apps = set(_installed_apps())
+
+ # Trim info for apps if not using --full
+ for app, infos in catalog["apps"].items():
+ infos["installed"] = app in installed_apps
+
+ infos["manifest"]["description"] = _value_for_locale(
+ infos["manifest"]["description"]
+ )
+
+ if not full:
+ catalog["apps"][app] = {
+ "description": infos["manifest"]["description"],
+ "level": infos["level"],
+ }
+ else:
+ infos["manifest"]["arguments"] = _set_default_ask_questions(
+ infos["manifest"].get("arguments", {})
+ )
+
+ # Trim info for categories if not using --full
+ for category in catalog["categories"]:
+ category["title"] = _value_for_locale(category["title"])
+ category["description"] = _value_for_locale(category["description"])
+ for subtags in category.get("subtags", []):
+ subtags["title"] = _value_for_locale(subtags["title"])
+
+ if not full:
+ catalog["categories"] = [
+ {"id": c["id"], "description": c["description"]}
+ for c in catalog["categories"]
+ ]
+
+ if not with_categories:
+ return {"apps": catalog["apps"]}
+ else:
+ return {"apps": catalog["apps"], "categories": catalog["categories"]}
+
+
+def app_search(string):
+ """
+ Return a dict of apps whose description or name match the search string
+ """
+
+ # Retrieve a simple dict listing all apps
+ catalog_of_apps = app_catalog()
+
+ # Selecting apps according to a match in app name or description
+ matching_apps = {"apps": {}}
+ for app in catalog_of_apps["apps"].items():
+ if re.search(string, app[0], flags=re.IGNORECASE) or re.search(
+ string, app[1]["description"], flags=re.IGNORECASE
+ ):
+ matching_apps["apps"][app[0]] = app[1]
+
+ return matching_apps
+
+
+def _initialize_apps_catalog_system():
+ """
+ This function is meant to intialize the apps_catalog system with YunoHost's default app catalog.
+ """
+
+ default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}]
+
+ try:
+ logger.debug(
+ "Initializing apps catalog system with YunoHost's default app list"
+ )
+ write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list)
+ except Exception as e:
+ raise YunohostError(
+ "Could not initialize the apps catalog system... : %s" % str(e)
+ )
+
+ logger.success(m18n.n("apps_catalog_init_success"))
+
+
+def _read_apps_catalog_list():
+ """
+ Read the json corresponding to the list of apps catalogs
+ """
+
+ try:
+ list_ = read_yaml(APPS_CATALOG_CONF)
+ # Support the case where file exists but is empty
+ # by returning [] if list_ is None
+ return list_ if list_ else []
+ except Exception as e:
+ raise YunohostError("Could not read the apps_catalog list ... : %s" % str(e))
+
+
+def _actual_apps_catalog_api_url(base_url):
+
+ return "{base_url}/v{version}/apps.json".format(
+ base_url=base_url, version=APPS_CATALOG_API_VERSION
+ )
+
+
+def _update_apps_catalog():
+ """
+ Fetches the json for each apps_catalog and update the cache
+
+ apps_catalog_list is for example :
+ [ {"id": "default", "url": "https://app.yunohost.org/default/"} ]
+
+ Then for each apps_catalog, the actual json URL to be fetched is like :
+ https://app.yunohost.org/default/vX/apps.json
+
+ And store it in :
+ /var/cache/yunohost/repo/default.json
+ """
+
+ apps_catalog_list = _read_apps_catalog_list()
+
+ logger.info(m18n.n("apps_catalog_updating"))
+
+ # Create cache folder if needed
+ if not os.path.exists(APPS_CATALOG_CACHE):
+ logger.debug("Initialize folder for apps catalog cache")
+ mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root")
+
+ for apps_catalog in apps_catalog_list:
+ apps_catalog_id = apps_catalog["id"]
+ actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"])
+
+ # Fetch the json
+ try:
+ apps_catalog_content = download_json(actual_api_url)
+ except Exception as e:
+ raise YunohostError(
+ "apps_catalog_failed_to_download",
+ apps_catalog=apps_catalog_id,
+ error=str(e),
+ )
+
+ # Remember the apps_catalog api version for later
+ apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION
+
+ # Save the apps_catalog data in the cache
+ cache_file = "{cache_folder}/{list}.json".format(
+ cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id
+ )
+ try:
+ write_to_json(cache_file, apps_catalog_content)
+ except Exception as e:
+ raise YunohostError(
+ "Unable to write cache data for %s apps_catalog : %s"
+ % (apps_catalog_id, str(e))
+ )
+
+ logger.success(m18n.n("apps_catalog_update_success"))
+
+
+def _load_apps_catalog():
+ """
+ Read all the apps catalog cache files and build a single dict (merged_catalog)
+ corresponding to all known apps and categories
+ """
+
+ merged_catalog = {"apps": {}, "categories": []}
+
+ for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]:
+
+ # Let's load the json from cache for this catalog
+ cache_file = "{cache_folder}/{list}.json".format(
+ cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id
+ )
+
+ try:
+ apps_catalog_content = (
+ read_json(cache_file) if os.path.exists(cache_file) else None
+ )
+ except Exception as e:
+ raise YunohostError(
+ "Unable to read cache for apps_catalog %s : %s" % (cache_file, e),
+ raw_msg=True,
+ )
+
+ # Check that the version of the data matches version ....
+ # ... otherwise it means we updated yunohost in the meantime
+ # and need to update the cache for everything to be consistent
+ if (
+ not apps_catalog_content
+ or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION
+ ):
+ logger.info(m18n.n("apps_catalog_obsolete_cache"))
+ _update_apps_catalog()
+ apps_catalog_content = read_json(cache_file)
+
+ del apps_catalog_content["from_api_version"]
+
+ # Add apps from this catalog to the output
+ for app, info in apps_catalog_content["apps"].items():
+
+ # (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ...
+ # in which case we keep only the first one found)
+ if app in merged_catalog["apps"]:
+ logger.warning(
+ "Duplicate app %s found between apps catalog %s and %s"
+ % (app, apps_catalog_id, merged_catalog["apps"][app]["repository"])
+ )
+ continue
+
+ info["repository"] = apps_catalog_id
+ merged_catalog["apps"][app] = info
+
+ # Annnnd categories
+ merged_catalog["categories"] += apps_catalog_content["categories"]
+
+ return merged_catalog
diff --git a/src/yunohost/authenticators/ldap_admin.py b/src/yunohost/authenticators/ldap_admin.py
new file mode 100644
index 000000000..94d68a8db
--- /dev/null
+++ b/src/yunohost/authenticators/ldap_admin.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+
+import os
+import logging
+import ldap
+import ldap.sasl
+import time
+
+from moulinette import m18n
+from moulinette.authentication import BaseAuthenticator
+from yunohost.utils.error import YunohostError
+
+logger = logging.getLogger("yunohost.authenticators.ldap_admin")
+
+
+class Authenticator(BaseAuthenticator):
+
+ name = "ldap_admin"
+
+ def __init__(self, *args, **kwargs):
+ self.uri = "ldap://localhost:389"
+ self.basedn = "dc=yunohost,dc=org"
+ self.admindn = "cn=admin,dc=yunohost,dc=org"
+
+ def _authenticate_credentials(self, credentials=None):
+
+ # TODO : change authentication format
+ # to support another dn to support multi-admins
+
+ def _reconnect():
+ con = ldap.ldapobject.ReconnectLDAPObject(
+ self.uri, retry_max=10, retry_delay=0.5
+ )
+ con.simple_bind_s(self.admindn, credentials)
+ return con
+
+ try:
+ con = _reconnect()
+ except ldap.INVALID_CREDENTIALS:
+ raise YunohostError("invalid_password")
+ except ldap.SERVER_DOWN:
+ # ldap is down, attempt to restart it before really failing
+ logger.warning(m18n.n("ldap_server_is_down_restart_it"))
+ os.system("systemctl restart slapd")
+ time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted
+
+ try:
+ con = _reconnect()
+ except ldap.SERVER_DOWN:
+ raise YunohostError("ldap_server_down")
+
+ # Check that we are indeed logged in with the expected identity
+ try:
+ # whoami_s return dn:..., then delete these 3 characters
+ who = con.whoami_s()[3:]
+ except Exception as e:
+ logger.warning("Error during ldap authentication process: %s", e)
+ raise
+ else:
+ if who != self.admindn:
+ raise YunohostError(
+ f"Not logged with the appropriate identity ? Found {who}, expected {self.admindn} !?",
+ raw_msg=True,
+ )
+ finally:
+ # Free the connection, we don't really need it to keep it open as the point is only to check authentication...
+ if con:
+ con.unbind_s()
diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py
index 50765ba5f..cce66597a 100644
--- a/src/yunohost/backup.py
+++ b/src/yunohost/backup.py
@@ -36,35 +36,39 @@ from datetime import datetime
from glob import glob
from collections import OrderedDict
from functools import reduce
+from packaging import version
-from moulinette import msignals, m18n, msettings
+from moulinette import Moulinette, m18n
from moulinette.utils import filesystem
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml
from moulinette.utils.process import check_output
+import yunohost.domain
from yunohost.app import (
app_info,
_is_installed,
_make_environment_for_app_script,
- dump_app_log_extract_for_debugging,
- _patch_legacy_helpers,
- _patch_legacy_php_versions,
- _patch_legacy_php_versions_in_settings,
- LEGACY_PHP_VERSION_REPLACEMENTS,
+ _make_tmp_workdir_for_app,
)
from yunohost.hook import (
hook_list,
hook_info,
hook_callback,
hook_exec,
+ hook_exec_with_script_debug_if_failure,
CUSTOM_HOOK_FOLDER,
)
-from yunohost.tools import tools_postinstall
+from yunohost.tools import (
+ tools_postinstall,
+ _tools_migrations_run_after_system_restore,
+ _tools_migrations_run_before_app_restore,
+)
from yunohost.regenconf import regen_conf
-from yunohost.log import OperationLogger
-from yunohost.utils.error import YunohostError
+from yunohost.log import OperationLogger, is_unit_operation
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.packages import ynh_packages_version
+from yunohost.utils.filesystem import free_space_in_directory
from yunohost.settings import settings_get
BACKUP_PATH = "/home/yunohost.backup"
@@ -348,7 +352,7 @@ class BackupManager:
# Try to recursively unmount stuff (from a previously failed backup ?)
if not _recursive_umount(self.work_dir):
- raise YunohostError("backup_output_directory_not_empty")
+ raise YunohostValidationError("backup_output_directory_not_empty")
else:
# If umount succeeded, remove the directory (we checked that
# we're in /home/yunohost.backup/tmp so that should be okay...
@@ -537,8 +541,8 @@ class BackupManager:
# Add unlisted files from backup tmp dir
self._add_to_list_to_backup("backup.csv")
self._add_to_list_to_backup("info.json")
- if len(self.apps_return) > 0:
- self._add_to_list_to_backup("apps")
+ for app in self.apps_return.keys():
+ self._add_to_list_to_backup(f"apps/{app}")
if os.path.isdir(os.path.join(self.work_dir, "conf")):
self._add_to_list_to_backup("conf")
if os.path.isdir(os.path.join(self.work_dir, "data")):
@@ -643,7 +647,7 @@ class BackupManager:
restore_hooks_dir = os.path.join(self.work_dir, "hooks", "restore")
if not os.path.exists(restore_hooks_dir):
- filesystem.mkdir(restore_hooks_dir, mode=0o750, parents=True, uid="admin")
+ filesystem.mkdir(restore_hooks_dir, mode=0o700, parents=True, uid="root")
restore_hooks = hook_list("restore")["hooks"]
@@ -662,7 +666,7 @@ class BackupManager:
self.targets.set_result("system", part, "Error")
def _collect_apps_files(self):
- """ Prepare backup for each selected apps """
+ """Prepare backup for each selected apps"""
apps_targets = self.targets.list("apps", exclude=["Skipped"])
@@ -700,39 +704,34 @@ class BackupManager:
# Prepare environment
env_dict = self._get_env_var(app)
+ env_dict["YNH_APP_BASEDIR"] = os.path.join(
+ self.work_dir, "apps", app, "settings"
+ )
tmp_app_bkp_dir = env_dict["YNH_APP_BACKUP_DIR"]
settings_dir = os.path.join(self.work_dir, "apps", app, "settings")
logger.info(m18n.n("app_start_backup", app=app))
- tmp_script = (
- None # This is to make sure the var exists later in the 'finally' ...
- )
+ tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app)
try:
# Prepare backup directory for the app
- filesystem.mkdir(tmp_app_bkp_dir, 0o750, True, uid="admin")
+ filesystem.mkdir(tmp_app_bkp_dir, 0o700, True, uid="root")
# Copy the app settings to be able to call _common.sh
shutil.copytree(app_setting_path, settings_dir)
- # Copy app backup script in a temporary folder and execute it
- _, tmp_script = tempfile.mkstemp(prefix="backup_")
- app_script = os.path.join(app_setting_path, "scripts/backup")
- subprocess.call(["install", "-Dm555", app_script, tmp_script])
-
hook_exec(
- tmp_script, raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict
+ f"{tmp_workdir_for_app}/scripts/backup",
+ raise_on_error=True,
+ chdir=tmp_app_bkp_dir,
+ env=env_dict,
)[0]
self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"])
# backup permissions
logger.debug(m18n.n("backup_permission", app=app))
- permissions = user_permission_list(full=True)["permissions"]
- this_app_permissions = {
- name: infos
- for name, infos in permissions.items()
- if name.startswith(app + ".")
- }
+ permissions = user_permission_list(full=True, apps=[app])["permissions"]
+ this_app_permissions = {name: infos for name, infos in permissions.items()}
write_to_yaml("%s/permissions.yml" % settings_dir, this_app_permissions)
except Exception:
@@ -752,8 +751,7 @@ class BackupManager:
# Remove tmp files in all situations
finally:
- if tmp_script:
- filesystem.rm(tmp_script, force=True)
+ shutil.rmtree(tmp_workdir_for_app)
filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True)
#
@@ -794,25 +792,28 @@ class BackupManager:
self.size_details["apps"][app_key] = 0
for row in self.paths_to_backup:
- if row["dest"] != "info.json":
- size = disk_usage(row["source"])
+ if row["dest"] == "info.json":
+ continue
- # Add size to apps details
- splitted_dest = row["dest"].split("/")
- category = splitted_dest[0]
- if category == "apps":
- for app_key in self.apps_return:
- if row["dest"].startswith("apps/" + app_key):
- self.size_details["apps"][app_key] += size
- break
- # OR Add size to the correct system element
- elif category == "data" or category == "conf":
- for system_key in self.system_return:
- if row["dest"].startswith(system_key.replace("_", "/")):
- self.size_details["system"][system_key] += size
- break
+ size = disk_usage(row["source"])
- self.size += size
+ # Add size to apps details
+ splitted_dest = row["dest"].split("/")
+ category = splitted_dest[0]
+ if category == "apps":
+ for app_key in self.apps_return:
+ if row["dest"].startswith("apps/" + app_key):
+ self.size_details["apps"][app_key] += size
+ break
+
+ # OR Add size to the correct system element
+ elif category == "data" or category == "conf":
+ for system_key in self.system_return:
+ if row["dest"].startswith(system_key.replace("_", "/")):
+ self.size_details["system"][system_key] += size
+ break
+
+ self.size += size
return self.size
@@ -860,6 +861,11 @@ class RestoreManager:
# FIXME this way to get the info is not compatible with copy or custom
# backup methods
self.info = backup_info(name, with_details=True)
+ if not self.info["from_yunohost_version"] or version.parse(
+ self.info["from_yunohost_version"]
+ ) < version.parse("3.8.0"):
+ raise YunohostValidationError("restore_backup_too_old")
+
self.archive_path = self.info["path"]
self.name = name
self.method = BackupMethod.create(method, self)
@@ -1029,7 +1035,7 @@ class RestoreManager:
already_installed = [app for app in to_be_restored if _is_installed(app)]
if already_installed != []:
if already_installed == to_be_restored:
- raise YunohostError(
+ raise YunohostValidationError(
"restore_already_installed_apps", apps=", ".join(already_installed)
)
else:
@@ -1135,14 +1141,14 @@ class RestoreManager:
return True
elif free_space > needed_space:
# TODO Add --force options to avoid the error raising
- raise YunohostError(
+ raise YunohostValidationError(
"restore_may_be_not_enough_disk_space",
free_space=free_space,
needed_space=needed_space,
margin=margin,
)
else:
- raise YunohostError(
+ raise YunohostValidationError(
"restore_not_enough_disk_space",
free_space=free_space,
needed_space=needed_space,
@@ -1180,6 +1186,7 @@ class RestoreManager:
"""
Apply dirty patch to redirect php5 and php7.0 files to php7.3
"""
+ from yunohost.utils.legacy import LEGACY_PHP_VERSION_REPLACEMENTS
backup_csv = os.path.join(self.work_dir, "backup.csv")
@@ -1209,7 +1216,7 @@ class RestoreManager:
writer.writerow(row)
def _restore_system(self):
- """ Restore user and system parts """
+ """Restore user and system parts"""
system_targets = self.targets.list("system", exclude=["Skipped"])
@@ -1217,7 +1224,6 @@ class RestoreManager:
if system_targets == []:
return
- from yunohost.user import user_group_list
from yunohost.permission import (
permission_create,
permission_delete,
@@ -1278,27 +1284,21 @@ class RestoreManager:
else:
operation_logger.success()
+ yunohost.domain.domain_list_cache = {}
+
regen_conf()
- # Check that at least a group exists (all_users) to know if we need to
- # do the migration 0011 : setup group and permission
- #
- # Legacy code
- if "all_users" not in user_group_list()["groups"].keys():
- from yunohost.utils.legacy import SetupGroupPermissions
+ _tools_migrations_run_after_system_restore(
+ backup_version=self.info["from_yunohost_version"]
+ )
- # Update LDAP schema restart slapd
- logger.info(m18n.n("migration_0011_update_LDAP_schema"))
- regen_conf(names=["slapd"], force=True)
- SetupGroupPermissions.migrate_LDAP_db()
-
- # Remove all permission for all app which is still in the LDAP
+ # Remove all permission for all app still in the LDAP
for permission_name in user_permission_list(ignore_system_perms=True)[
"permissions"
].keys():
permission_delete(permission_name, force=True, sync_perm=False)
- # Restore permission for the app which is installed
+ # Restore permission for apps installed
for permission_name, permission_infos in old_apps_permission.items():
app_name, perm_name = permission_name.split(".")
if _is_installed(app_name):
@@ -1348,8 +1348,12 @@ class RestoreManager:
app_instance_name -- (string) The app name to restore (no app with this
name should be already install)
"""
+ from yunohost.utils.legacy import (
+ _patch_legacy_php_versions,
+ _patch_legacy_php_versions_in_settings,
+ _patch_legacy_helpers,
+ )
from yunohost.user import user_group_list
- from yunohost.app import app_setting
from yunohost.permission import (
permission_create,
permission_delete,
@@ -1402,7 +1406,6 @@ class RestoreManager:
self.targets.set_result("apps", app_instance_name, "Warning")
return
- logger.debug(m18n.n("restore_running_app_script", app=app_instance_name))
try:
# Restore app settings
app_settings_new_path = os.path.join(
@@ -1411,154 +1414,161 @@ class RestoreManager:
app_scripts_new_path = os.path.join(app_settings_new_path, "scripts")
shutil.copytree(app_settings_in_archive, app_settings_new_path)
filesystem.chmod(app_settings_new_path, 0o400, 0o400, True)
- filesystem.chown(app_scripts_new_path, "admin", None, True)
+ filesystem.chown(app_scripts_new_path, "root", None, True)
# Copy the app scripts to a writable temporary folder
- # FIXME : use 'install -Dm555' or something similar to what's done
- # in the backup method ?
- tmp_folder_for_app_restore = tempfile.mkdtemp(prefix="restore")
- copytree(app_scripts_in_archive, tmp_folder_for_app_restore)
- filesystem.chmod(tmp_folder_for_app_restore, 0o550, 0o550, True)
- filesystem.chown(tmp_folder_for_app_restore, "admin", None, True)
- restore_script = os.path.join(tmp_folder_for_app_restore, "restore")
+ tmp_workdir_for_app = _make_tmp_workdir_for_app()
+ copytree(app_scripts_in_archive, tmp_workdir_for_app)
+ filesystem.chmod(tmp_workdir_for_app, 0o700, 0o700, True)
+ filesystem.chown(tmp_workdir_for_app, "root", None, True)
+ restore_script = os.path.join(tmp_workdir_for_app, "restore")
# Restore permissions
- if os.path.isfile("%s/permissions.yml" % app_settings_new_path):
+ if not os.path.isfile("%s/permissions.yml" % app_settings_new_path):
+ raise YunohostError(
+ "Didnt find a permssions.yml for the app !?", raw_msg=True
+ )
- permissions = read_yaml("%s/permissions.yml" % app_settings_new_path)
- existing_groups = user_group_list()["groups"]
+ permissions = read_yaml("%s/permissions.yml" % app_settings_new_path)
+ existing_groups = user_group_list()["groups"]
- for permission_name, permission_infos in permissions.items():
+ for permission_name, permission_infos in permissions.items():
- if "allowed" not in permission_infos:
- logger.warning(
- "'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself."
- % (permission_name, app_instance_name)
- )
- should_be_allowed = ["all_users"]
- else:
- should_be_allowed = [
- g
- for g in permission_infos["allowed"]
- if g in existing_groups
- ]
-
- perm_name = permission_name.split(".")[1]
- permission_create(
- permission_name,
- allowed=should_be_allowed,
- url=permission_infos.get("url"),
- additional_urls=permission_infos.get("additional_urls"),
- auth_header=permission_infos.get("auth_header"),
- label=permission_infos.get("label")
- if perm_name == "main"
- else permission_infos.get("sublabel"),
- show_tile=permission_infos.get("show_tile", True),
- protected=permission_infos.get("protected", False),
- sync_perm=False,
+ if "allowed" not in permission_infos:
+ logger.warning(
+ "'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself."
+ % (permission_name, app_instance_name)
)
+ should_be_allowed = ["all_users"]
+ else:
+ should_be_allowed = [
+ g for g in permission_infos["allowed"] if g in existing_groups
+ ]
- permission_sync_to_user()
+ perm_name = permission_name.split(".")[1]
+ permission_create(
+ permission_name,
+ allowed=should_be_allowed,
+ url=permission_infos.get("url"),
+ additional_urls=permission_infos.get("additional_urls"),
+ auth_header=permission_infos.get("auth_header"),
+ label=permission_infos.get("label")
+ if perm_name == "main"
+ else permission_infos.get("sublabel"),
+ show_tile=permission_infos.get("show_tile", True),
+ protected=permission_infos.get("protected", False),
+ sync_perm=False,
+ )
- os.remove("%s/permissions.yml" % app_settings_new_path)
- else:
- # Otherwise, we need to migrate the legacy permissions of this
- # app (included in its settings.yml)
- from yunohost.utils.legacy import SetupGroupPermissions
+ permission_sync_to_user()
- SetupGroupPermissions.migrate_app_permission(app=app_instance_name)
+ os.remove("%s/permissions.yml" % app_settings_new_path)
- # Migrate old settings
- legacy_permission_settings = [
- "skipped_uris",
- "unprotected_uris",
- "protected_uris",
- "skipped_regex",
- "unprotected_regex",
- "protected_regex",
- ]
- if any(
- app_setting(app_instance_name, setting) is not None
- for setting in legacy_permission_settings
- ):
- from yunohost.utils.legacy import migrate_legacy_permission_settings
-
- migrate_legacy_permission_settings(app=app_instance_name)
-
- # Prepare env. var. to pass to script
- env_dict = _make_environment_for_app_script(app_instance_name)
- env_dict.update(
- {
- "YNH_BACKUP_DIR": self.work_dir,
- "YNH_BACKUP_CSV": os.path.join(self.work_dir, "backup.csv"),
- "YNH_APP_BACKUP_DIR": os.path.join(
- self.work_dir, "apps", app_instance_name, "backup"
- ),
- }
+ _tools_migrations_run_before_app_restore(
+ backup_version=self.info["from_yunohost_version"],
+ app_id=app_instance_name,
)
-
- operation_logger.extra["env"] = env_dict
- operation_logger.flush()
-
- # Execute app restore script
- hook_exec(
- restore_script,
- chdir=app_backup_in_archive,
- raise_on_error=True,
- env=env_dict,
- )[0]
except Exception:
- msg = m18n.n("restore_app_failed", app=app_instance_name)
+ import traceback
+
+ error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
+ msg = m18n.n("app_restore_failed", app=app_instance_name, error=error)
logger.error(msg)
operation_logger.error(msg)
- if msettings.get("interface") != "api":
- dump_app_log_extract_for_debugging(operation_logger)
-
self.targets.set_result("apps", app_instance_name, "Error")
- remove_script = os.path.join(app_scripts_in_archive, "remove")
-
- # Setup environment for remove script
- env_dict_remove = _make_environment_for_app_script(app_instance_name)
-
- operation_logger = OperationLogger(
- "remove_on_failed_restore",
- [("app", app_instance_name)],
- env=env_dict_remove,
- )
- operation_logger.start()
-
- # Execute remove script
- if hook_exec(remove_script, env=env_dict_remove)[0] != 0:
- msg = m18n.n("app_not_properly_removed", app=app_instance_name)
- logger.warning(msg)
- operation_logger.error(msg)
- else:
- operation_logger.success()
-
- # Cleaning app directory
+ # Cleanup
shutil.rmtree(app_settings_new_path, ignore_errors=True)
+ shutil.rmtree(tmp_workdir_for_app, ignore_errors=True)
- # Remove all permission in LDAP for this app
- for permission_name in user_permission_list()["permissions"].keys():
- if permission_name.startswith(app_instance_name + "."):
- permission_delete(permission_name, force=True)
+ return
- # TODO Cleaning app hooks
- else:
- self.targets.set_result("apps", app_instance_name, "Success")
- operation_logger.success()
+ logger.debug(m18n.n("restore_running_app_script", app=app_instance_name))
+
+ # Prepare env. var. to pass to script
+ # FIXME : workdir should be a tmp workdir
+ app_workdir = os.path.join(self.work_dir, "apps", app_instance_name, "settings")
+ env_dict = _make_environment_for_app_script(
+ app_instance_name, workdir=app_workdir
+ )
+ env_dict.update(
+ {
+ "YNH_BACKUP_DIR": self.work_dir,
+ "YNH_BACKUP_CSV": os.path.join(self.work_dir, "backup.csv"),
+ "YNH_APP_BACKUP_DIR": os.path.join(
+ self.work_dir, "apps", app_instance_name, "backup"
+ ),
+ }
+ )
+
+ operation_logger.extra["env"] = env_dict
+ operation_logger.flush()
+
+ # Execute the app install script
+ restore_failed = True
+ try:
+ (
+ restore_failed,
+ failure_message_with_debug_instructions,
+ ) = hook_exec_with_script_debug_if_failure(
+ restore_script,
+ chdir=app_backup_in_archive,
+ env=env_dict,
+ operation_logger=operation_logger,
+ error_message_if_script_failed=m18n.n("app_restore_script_failed"),
+ error_message_if_failed=lambda e: m18n.n(
+ "app_restore_failed", app=app_instance_name, error=e
+ ),
+ )
finally:
# Cleaning temporary scripts directory
- shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True)
+ shutil.rmtree(tmp_workdir_for_app, ignore_errors=True)
+
+ if not restore_failed:
+ self.targets.set_result("apps", app_instance_name, "Success")
+ operation_logger.success()
+ else:
+
+ self.targets.set_result("apps", app_instance_name, "Error")
+
+ remove_script = os.path.join(app_scripts_in_archive, "remove")
+
+ # Setup environment for remove script
+ env_dict_remove = _make_environment_for_app_script(
+ app_instance_name, workdir=app_workdir
+ )
+ remove_operation_logger = OperationLogger(
+ "remove_on_failed_restore",
+ [("app", app_instance_name)],
+ env=env_dict_remove,
+ )
+ remove_operation_logger.start()
+
+ # Execute remove script
+ if hook_exec(remove_script, env=env_dict_remove)[0] != 0:
+ msg = m18n.n("app_not_properly_removed", app=app_instance_name)
+ logger.warning(msg)
+ remove_operation_logger.error(msg)
+ else:
+ remove_operation_logger.success()
+
+ # Cleaning app directory
+ shutil.rmtree(app_settings_new_path, ignore_errors=True)
+
+ # Remove all permission in LDAP for this app
+ for permission_name in user_permission_list()["permissions"].keys():
+ if permission_name.startswith(app_instance_name + "."):
+ permission_delete(permission_name, force=True)
+
+ # TODO Cleaning app hooks
+
+ logger.error(failure_message_with_debug_instructions)
#
# Backup methods #
#
-
-
class BackupMethod(object):
"""
@@ -1731,7 +1741,7 @@ class BackupMethod(object):
free_space,
backup_size,
)
- raise YunohostError("not_enough_disk_space", path=self.repo)
+ raise YunohostValidationError("not_enough_disk_space", path=self.repo)
def _organize_files(self):
"""
@@ -1825,7 +1835,7 @@ class BackupMethod(object):
# Ask confirmation for copying
if size > MB_ALLOWED_TO_ORGANIZE:
try:
- i = msignals.prompt(
+ i = Moulinette.prompt(
m18n.n(
"backup_ask_for_copying_if_needed",
answers="y/N",
@@ -1858,7 +1868,7 @@ class CopyBackupMethod(BackupMethod):
method_name = "copy"
def backup(self):
- """ Copy prepared files into a the repo """
+ """Copy prepared files into a the repo"""
# Check free space in output
self._check_is_enough_free_space()
@@ -1871,7 +1881,7 @@ class CopyBackupMethod(BackupMethod):
dest_parent = os.path.dirname(dest)
if not os.path.exists(dest_parent):
- filesystem.mkdir(dest_parent, 0o750, True, uid="admin")
+ filesystem.mkdir(dest_parent, 0o700, True, uid="admin")
if os.path.isdir(source):
shutil.copytree(source, dest)
@@ -2003,7 +2013,7 @@ class TarBackupMethod(BackupMethod):
try:
files_in_archive = tar.getnames()
- except IOError as e:
+ except (IOError, EOFError, tarfile.ReadError) as e:
raise YunohostError(
"backup_archive_corrupted", archive=self._archive_file, error=str(e)
)
@@ -2165,8 +2175,16 @@ class CustomBackupMethod(BackupMethod):
#
+@is_unit_operation()
def backup_create(
- name=None, description=None, methods=[], output_directory=None, system=[], apps=[]
+ operation_logger,
+ name=None,
+ description=None,
+ methods=[],
+ output_directory=None,
+ system=[],
+ apps=[],
+ dry_run=False,
):
"""
Create a backup local archive
@@ -2188,7 +2206,7 @@ def backup_create(
# Validate there is no archive with the same name
if name and name in backup_list()["archives"]:
- raise YunohostError("backup_archive_name_exists")
+ raise YunohostValidationError("backup_archive_name_exists")
# By default we backup using the tar method
if not methods:
@@ -2203,14 +2221,14 @@ def backup_create(
r"^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$",
output_directory,
):
- raise YunohostError("backup_output_directory_forbidden")
+ raise YunohostValidationError("backup_output_directory_forbidden")
if "copy" in methods:
if not output_directory:
- raise YunohostError("backup_output_directory_required")
+ raise YunohostValidationError("backup_output_directory_required")
# Check that output directory is empty
elif os.path.isdir(output_directory) and os.listdir(output_directory):
- raise YunohostError("backup_output_directory_not_empty")
+ raise YunohostValidationError("backup_output_directory_not_empty")
# If no --system or --apps given, backup everything
if system is None and apps is None:
@@ -2221,6 +2239,8 @@ def backup_create(
# Intialize #
#
+ operation_logger.start()
+
# Create yunohost archives directory if it does not exists
_create_archive_dir()
@@ -2235,6 +2255,10 @@ def backup_create(
backup_manager.set_system_targets(system)
backup_manager.set_apps_targets(apps)
+ for app in backup_manager.targets.list("apps", exclude=["Skipped"]):
+ operation_logger.related_to.append(("app", app))
+ operation_logger.flush()
+
#
# Collect files and put them in the archive #
#
@@ -2242,11 +2266,24 @@ def backup_create(
# Collect files to be backup (by calling app backup script / system hooks)
backup_manager.collect_files()
+ if dry_run:
+ return {
+ "size": backup_manager.size,
+ "size_details": backup_manager.size_details,
+ }
+
# Apply backup methods on prepared files
logger.info(m18n.n("backup_actually_backuping"))
+ logger.info(
+ m18n.n(
+ "backup_create_size_estimation",
+ size=binary_to_human(backup_manager.size) + "B",
+ )
+ )
backup_manager.backup()
logger.success(m18n.n("backup_created"))
+ operation_logger.success()
return {
"name": backup_manager.name,
@@ -2279,6 +2316,11 @@ def backup_restore(name, system=[], apps=[], force=False):
# Initialize #
#
+ if name.endswith(".tar.gz"):
+ name = name[: -len(".tar.gz")]
+ elif name.endswith(".tar"):
+ name = name[: -len(".tar")]
+
restore_manager = RestoreManager(name)
restore_manager.set_system_targets(system)
@@ -2297,7 +2339,7 @@ def backup_restore(name, system=[], apps=[], force=False):
if not force:
try:
# Ask confirmation for restoring
- i = msignals.prompt(
+ i = Moulinette.prompt(
m18n.n("restore_confirm_yunohost_installed", answers="y/N")
)
except NotImplemented:
@@ -2371,7 +2413,7 @@ def backup_list(with_info=False, human_readable=False):
def backup_download(name):
- if msettings.get("interface") != "api":
+ if Moulinette.interface.type != "api":
logger.error(
"This option is only meant for the API/webadmin and doesn't make sense for the command line."
)
@@ -2383,7 +2425,7 @@ def backup_download(name):
if not os.path.lexists(archive_file):
archive_file += ".gz"
if not os.path.lexists(archive_file):
- raise YunohostError("backup_archive_name_unknown", name=name)
+ raise YunohostValidationError("backup_archive_name_unknown", name=name)
# If symlink, retrieve the real path
if os.path.islink(archive_file):
@@ -2391,7 +2433,9 @@ def backup_download(name):
# Raise exception if link is broken (e.g. on unmounted external storage)
if not os.path.exists(archive_file):
- raise YunohostError("backup_archive_broken_link", path=archive_file)
+ raise YunohostValidationError(
+ "backup_archive_broken_link", path=archive_file
+ )
# We return a raw bottle HTTPresponse (instead of serializable data like
# list/dict, ...), which is gonna be picked and used directly by moulinette
@@ -2411,13 +2455,19 @@ def backup_info(name, with_details=False, human_readable=False):
human_readable -- Print sizes in human readable format
"""
+
+ if name.endswith(".tar.gz"):
+ name = name[: -len(".tar.gz")]
+ elif name.endswith(".tar"):
+ name = name[: -len(".tar")]
+
archive_file = "%s/%s.tar" % (ARCHIVES_PATH, name)
# Check file exist (even if it's a broken symlink)
if not os.path.lexists(archive_file):
archive_file += ".gz"
if not os.path.lexists(archive_file):
- raise YunohostError("backup_archive_name_unknown", name=name)
+ raise YunohostValidationError("backup_archive_name_unknown", name=name)
# If symlink, retrieve the real path
if os.path.islink(archive_file):
@@ -2425,7 +2475,9 @@ def backup_info(name, with_details=False, human_readable=False):
# Raise exception if link is broken (e.g. on unmounted external storage)
if not os.path.exists(archive_file):
- raise YunohostError("backup_archive_broken_link", path=archive_file)
+ raise YunohostValidationError(
+ "backup_archive_broken_link", path=archive_file
+ )
info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name)
@@ -2437,7 +2489,7 @@ def backup_info(name, with_details=False, human_readable=False):
try:
files_in_archive = tar.getnames()
- except IOError as e:
+ except (IOError, EOFError, tarfile.ReadError) as e:
raise YunohostError(
"backup_archive_corrupted", archive=archive_file, error=str(e)
)
@@ -2521,6 +2573,7 @@ def backup_info(name, with_details=False, human_readable=False):
result["apps"] = info["apps"]
result["system"] = info[system_key]
+ result["from_yunohost_version"] = info.get("from_yunohost_version")
return result
@@ -2533,7 +2586,7 @@ def backup_delete(name):
"""
if name not in backup_list()["archives"]:
- raise YunohostError("backup_archive_name_unknown", name=name)
+ raise YunohostValidationError("backup_archive_name_unknown", name=name)
hook_callback("pre_backup_delete", args=[name])
@@ -2550,6 +2603,8 @@ def backup_delete(name):
files_to_delete.append(actual_archive)
for backup_file in files_to_delete:
+ if not os.path.exists(backup_file):
+ continue
try:
os.remove(backup_file)
except Exception:
@@ -2567,7 +2622,7 @@ def backup_delete(name):
def _create_archive_dir():
- """ Create the YunoHost archives directory if doesn't exist """
+ """Create the YunoHost archives directory if doesn't exist"""
if not os.path.isdir(ARCHIVES_PATH):
if os.path.lexists(ARCHIVES_PATH):
raise YunohostError("backup_output_symlink_dir_broken", path=ARCHIVES_PATH)
@@ -2578,7 +2633,7 @@ def _create_archive_dir():
def _call_for_each_path(self, callback, csv_path=None):
- """ Call a callback for each path in csv """
+ """Call a callback for each path in csv"""
if csv_path is None:
csv_path = self.csv_path
with open(csv_path, "r") as backup_file:
@@ -2599,7 +2654,7 @@ def _recursive_umount(directory):
points_to_umount = [
line.split(" ")[2]
for line in mount_lines
- if len(line) >= 3 and line.split(" ")[2].startswith(directory)
+ if len(line) >= 3 and line.split(" ")[2].startswith(os.path.realpath(directory))
]
everything_went_fine = True
@@ -2613,11 +2668,6 @@ def _recursive_umount(directory):
return everything_went_fine
-def free_space_in_directory(dirpath):
- stat = os.statvfs(dirpath)
- return stat.f_frsize * stat.f_bavail
-
-
def disk_usage(path):
# We don't do this in python with os.stat because we don't want
# to follow symlinks
diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py
index f97cb42e5..a3cb3b3b6 100644
--- a/src/yunohost/certificate.py
+++ b/src/yunohost/certificate.py
@@ -37,7 +37,7 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file
from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.network import get_public_ip
from yunohost.diagnosis import Diagnoser
@@ -86,11 +86,8 @@ def certificate_status(domain_list, full=False):
domain_list = yunohost.domain.domain_list()["domains"]
# Else, validate that yunohost knows the domains given
else:
- yunohost_domains_list = yunohost.domain.domain_list()["domains"]
for domain in domain_list:
- # Is it in Yunohost domain list?
- if domain not in yunohost_domains_list:
- raise YunohostError("domain_name_unknown", domain=domain)
+ yunohost.domain._assert_domain_exists(domain)
certificates = {}
@@ -166,7 +163,7 @@ def _certificate_install_selfsigned(domain_list, force=False):
status = _get_status(domain)
if status["summary"]["code"] in ("good", "great"):
- raise YunohostError(
+ raise YunohostValidationError(
"certmanager_attempt_to_replace_valid_cert", domain=domain
)
@@ -197,6 +194,8 @@ def _certificate_install_selfsigned(domain_list, force=False):
out, _ = p.communicate()
+ out = out.decode("utf-8")
+
if p.returncode != 0:
logger.warning(out)
raise YunohostError("domain_cert_gen_failed")
@@ -265,14 +264,12 @@ def _certificate_install_letsencrypt(
# Else, validate that yunohost knows the domains given
else:
for domain in domain_list:
- yunohost_domains_list = yunohost.domain.domain_list()["domains"]
- if domain not in yunohost_domains_list:
- raise YunohostError("domain_name_unknown", domain=domain)
+ yunohost.domain._assert_domain_exists(domain)
# Is it self-signed?
status = _get_status(domain)
if not force and status["CA_type"]["code"] != "self-signed":
- raise YunohostError(
+ raise YunohostValidationError(
"certmanager_domain_cert_not_selfsigned", domain=domain
)
@@ -315,8 +312,6 @@ def _certificate_install_letsencrypt(
% domain
)
else:
- _install_cron(no_checks=no_checks)
-
logger.success(m18n.n("certmanager_cert_install_success", domain=domain))
operation_logger.success()
@@ -368,27 +363,26 @@ def certificate_renew(
else:
for domain in domain_list:
- # Is it in Yunohost dmomain list?
- if domain not in yunohost.domain.domain_list()["domains"]:
- raise YunohostError("domain_name_unknown", domain=domain)
+ # Is it in Yunohost domain list?
+ yunohost.domain._assert_domain_exists(domain)
status = _get_status(domain)
# Does it expire soon?
if status["validity"] > VALIDITY_LIMIT and not force:
- raise YunohostError(
+ raise YunohostValidationError(
"certmanager_attempt_to_renew_valid_cert", domain=domain
)
# Does it have a Let's Encrypt cert?
if status["CA_type"]["code"] != "lets-encrypt":
- raise YunohostError(
+ raise YunohostValidationError(
"certmanager_attempt_to_renew_nonLE_cert", domain=domain
)
# Check ACME challenge configured for given domain
if not _check_acme_challenge_configuration(domain):
- raise YunohostError(
+ raise YunohostValidationError(
"certmanager_acme_not_configured_for_domain", domain=domain
)
@@ -432,7 +426,7 @@ def certificate_renew(
stack = StringIO()
traceback.print_exc(file=stack)
- msg = "Certificate renewing for %s failed !" % (domain)
+ msg = "Certificate renewing for %s failed!" % (domain)
if no_checks:
msg += (
"\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s."
@@ -456,31 +450,6 @@ def certificate_renew(
#
-def _install_cron(no_checks=False):
- cron_job_file = "/etc/cron.daily/yunohost-certificate-renew"
-
- # we need to check if "--no-checks" isn't already put inside the existing
- # crontab, if it's the case it's probably because another domain needed it
- # at some point so we keep it
- if not no_checks and os.path.exists(cron_job_file):
- with open(cron_job_file, "r") as f:
- # no the best test in the world but except if we uses a shell
- # script parser I'm not expected a much more better way to do that
- no_checks = "--no-checks" in f.read()
-
- command = "yunohost domain cert-renew --email\n"
-
- if no_checks:
- # handle trailing "\n with ":-1"
- command = command[:-1] + " --no-checks\n"
-
- with open(cron_job_file, "w") as f:
- f.write("#!/bin/bash\n")
- f.write(command)
-
- _set_permissions(cron_job_file, "root", "root", 0o755)
-
-
def _email_renewing_failed(domain, exception_message, stack=""):
from_ = "certmanager@%s (Certificate Manager)" % domain
to_ = "root"
@@ -524,7 +493,7 @@ Subject: %s
import smtplib
smtp = smtplib.SMTP("localhost")
- smtp.sendmail(from_, [to_], message)
+ smtp.sendmail(from_, [to_], message.encode("utf-8"))
smtp.quit()
@@ -887,14 +856,9 @@ def _backup_current_cert(domain):
def _check_domain_is_ready_for_ACME(domain):
- dnsrecords = (
- Diagnoser.get_cached_report(
- "dnsrecords",
- item={"domain": domain, "category": "basic"},
- warn_if_no_cache=False,
- )
- or {}
- )
+ from yunohost.domain import _get_parent_domain_of
+ from yunohost.dns import _get_dns_zone_for_domain
+
httpreachable = (
Diagnoser.get_cached_report(
"web", item={"domain": domain}, warn_if_no_cache=False
@@ -902,21 +866,56 @@ def _check_domain_is_ready_for_ACME(domain):
or {}
)
- if not dnsrecords or not httpreachable:
- raise YunohostError("certmanager_domain_not_diagnosed_yet", domain=domain)
+ parent_domain = _get_parent_domain_of(domain)
+
+ dnsrecords = (
+ Diagnoser.get_cached_report(
+ "dnsrecords",
+ item={"domain": parent_domain, "category": "basic"},
+ warn_if_no_cache=False,
+ )
+ or {}
+ )
+
+ base_dns_zone = _get_dns_zone_for_domain(domain)
+ record_name = (
+ domain.replace(f".{base_dns_zone}", "") if domain != base_dns_zone else "@"
+ )
+ A_record_status = dnsrecords.get("data").get(f"A:{record_name}")
+ AAAA_record_status = dnsrecords.get("data").get(f"AAAA:{record_name}")
+
+ # Fallback to wildcard in case no result yet for the DNS name?
+ if not A_record_status:
+ A_record_status = dnsrecords.get("data").get("A:*")
+ if not AAAA_record_status:
+ AAAA_record_status = dnsrecords.get("data").get("AAAA:*")
+
+ if (
+ not httpreachable
+ or not dnsrecords.get("data")
+ or (A_record_status, AAAA_record_status) == (None, None)
+ ):
+ raise YunohostValidationError(
+ "certmanager_domain_not_diagnosed_yet", domain=domain
+ )
# Check if IP from DNS matches public IP
- if not dnsrecords.get("status") in [
- "SUCCESS",
- "WARNING",
- ]: # Warning is for missing IPv6 record which ain't critical for ACME
- raise YunohostError(
+ # - 'MISSING' for IPv6 ain't critical for ACME
+ # - IPv4 can be None assuming there's at least an IPv6, and viveversa
+ # - (the case where both are None is checked before)
+ if not (
+ A_record_status in [None, "OK"]
+ and AAAA_record_status in [None, "OK", "MISSING"]
+ ):
+ raise YunohostValidationError(
"certmanager_domain_dns_ip_differs_from_public_ip", domain=domain
)
# Check if domain seems to be accessible through HTTP?
if not httpreachable.get("status") == "SUCCESS":
- raise YunohostError("certmanager_domain_http_not_working", domain=domain)
+ raise YunohostValidationError(
+ "certmanager_domain_http_not_working", domain=domain
+ )
# FIXME / TODO : ideally this should not be needed. There should be a proper
@@ -975,11 +974,6 @@ def _name_self_CA():
def _tail(n, file_path):
- stdin, stdout = os.popen2("tail -n %s '%s'" % (n, file_path))
+ from moulinette.utils.process import check_output
- stdin.close()
-
- lines = stdout.readlines()
- stdout.close()
-
- return "".join(lines)
+ return check_output(f"tail -n {n} '{file_path}'")
diff --git a/src/yunohost/data_migrations/0015_migrate_to_buster.py b/src/yunohost/data_migrations/0015_migrate_to_buster.py
index e87c83087..4f2d4caf8 100644
--- a/src/yunohost/data_migrations/0015_migrate_to_buster.py
+++ b/src/yunohost/data_migrations/0015_migrate_to_buster.py
@@ -43,7 +43,7 @@ class MyMigration(Migration):
#
logger.info(m18n.n("migration_0015_patching_sources_list"))
self.patch_apt_sources_list()
- tools_update(system=True)
+ tools_update(target="system")
# Tell libc6 it's okay to restart system stuff during the upgrade
os.system(
@@ -88,7 +88,7 @@ class MyMigration(Migration):
apps_packages = self.get_apps_equivs_packages()
self.hold(apps_packages)
- tools_upgrade(system=True, allow_yunohost_upgrade=False)
+ tools_upgrade(target="system", allow_yunohost_upgrade=False)
if self.debian_major_version() == 9:
raise YunohostError("migration_0015_still_on_stretch_after_main_upgrade")
@@ -103,7 +103,7 @@ class MyMigration(Migration):
#
logger.info(m18n.n("migration_0015_yunohost_upgrade"))
self.unhold(apps_packages)
- tools_upgrade(system=True)
+ tools_upgrade(target="system")
def debian_major_version(self):
# The python module "platform" and lsb_release are not reliable because
@@ -141,7 +141,7 @@ class MyMigration(Migration):
# (but we don't if 'stretch' is already in the sources.list ...
# which means maybe a previous upgrade crashed and we're re-running it)
if " buster " not in read_file("/etc/apt/sources.list"):
- tools_update(system=True)
+ tools_update(target="system")
upgradable_system_packages = list(_list_upgradable_apt_packages())
if upgradable_system_packages:
raise YunohostError("migration_0015_system_not_fully_up_to_date")
diff --git a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py b/src/yunohost/data_migrations/0016_php70_to_php73_pools.py
index 6b424f211..fed96c9c8 100644
--- a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py
+++ b/src/yunohost/data_migrations/0016_php70_to_php73_pools.py
@@ -4,7 +4,8 @@ from shutil import copy2
from moulinette.utils.log import getActionLogger
-from yunohost.app import _is_installed, _patch_legacy_php_versions_in_settings
+from yunohost.app import _is_installed
+from yunohost.utils.legacy import _patch_legacy_php_versions_in_settings
from yunohost.tools import Migration
from yunohost.service import _run_service_command
diff --git a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py
index 728ae443f..1ccf5ccc9 100644
--- a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py
+++ b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py
@@ -1,7 +1,7 @@
import subprocess
from moulinette import m18n
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
@@ -23,7 +23,7 @@ class MyMigration(Migration):
return
if not self.package_is_installed("postgresql-11"):
- raise YunohostError("migration_0017_postgresql_11_not_installed")
+ raise YunohostValidationError("migration_0017_postgresql_11_not_installed")
# Make sure there's a 9.6 cluster
try:
@@ -37,7 +37,7 @@ class MyMigration(Migration):
if not space_used_by_directory(
"/var/lib/postgresql/9.6"
) > free_space_in_directory("/var/lib/postgresql"):
- raise YunohostError(
+ raise YunohostValidationError(
"migration_0017_not_enough_space", path="/var/lib/postgresql/"
)
@@ -78,5 +78,5 @@ class MyMigration(Migration):
)
)
- out = out.strip().split("\n")
+ out = out.strip().split(b"\n")
return (returncode, out, err)
diff --git a/src/yunohost/data_migrations/0018_xtable_to_nftable.py b/src/yunohost/data_migrations/0018_xtable_to_nftable.py
index af5d11e43..94b47d944 100644
--- a/src/yunohost/data_migrations/0018_xtable_to_nftable.py
+++ b/src/yunohost/data_migrations/0018_xtable_to_nftable.py
@@ -122,5 +122,5 @@ class MyMigration(Migration):
)
)
- out = out.strip().split("\n")
+ out = out.strip().split(b"\n")
return (returncode, out, err)
diff --git a/src/yunohost/data_migrations/0019_extend_permissions_features.py b/src/yunohost/data_migrations/0019_extend_permissions_features.py
index 07f740a2b..5d4343deb 100644
--- a/src/yunohost/data_migrations/0019_extend_permissions_features.py
+++ b/src/yunohost/data_migrations/0019_extend_permissions_features.py
@@ -1,8 +1,4 @@
-import time
-import os
-
from moulinette import m18n
-from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
@@ -17,7 +13,14 @@ class MyMigration(Migration):
Add protected attribute in LDAP permission
"""
- required = True
+ @Migration.ldap_migration
+ def run(self, backup_folder):
+
+ # Update LDAP database
+ self.add_new_ldap_attributes()
+
+ # Migrate old settings
+ migrate_legacy_permission_settings()
def add_new_ldap_attributes(self):
@@ -36,7 +39,7 @@ class MyMigration(Migration):
)
# Update LDAP schema restart slapd
- logger.info(m18n.n("migration_0011_update_LDAP_schema"))
+ logger.info(m18n.n("migration_update_LDAP_schema"))
regen_conf(names=["slapd"], force=True)
logger.info(m18n.n("migration_0019_add_new_attributes_in_ldap"))
@@ -78,54 +81,27 @@ class MyMigration(Migration):
ldap.update("cn=%s,ou=permission" % permission, update)
- def run(self):
+ introduced_in_version = "4.1"
- # FIXME : what do we really want to do here ...
- # Imho we should just force-regen the conf in all case, and maybe
- # just display a warning if we detect that the conf was manually modified
+ def run_after_system_restore(self):
+ # Update LDAP database
+ self.add_new_ldap_attributes()
- # Backup LDAP and the apps settings before to do the migration
- logger.info(m18n.n("migration_0019_backup_before_migration"))
- try:
- backup_folder = "/home/yunohost.backup/premigration/" + time.strftime(
- "%Y%m%d-%H%M%S", time.gmtime()
- )
- os.makedirs(backup_folder, 0o750)
- os.system("systemctl stop slapd")
- os.system("cp -r --preserve /etc/ldap %s/ldap_config" % backup_folder)
- os.system("cp -r --preserve /var/lib/ldap %s/ldap_db" % backup_folder)
- os.system(
- "cp -r --preserve /etc/yunohost/apps %s/apps_settings" % backup_folder
- )
- except Exception as e:
- raise YunohostError(
- "migration_0019_can_not_backup_before_migration", error=e
- )
- finally:
- os.system("systemctl start slapd")
+ def run_before_app_restore(self, app_id):
+ from yunohost.app import app_setting
+ from yunohost.utils.legacy import migrate_legacy_permission_settings
- try:
- # Update LDAP database
- self.add_new_ldap_attributes()
-
- # Migrate old settings
- migrate_legacy_permission_settings()
-
- except Exception:
- logger.warn(m18n.n("migration_0019_migration_failed_trying_to_rollback"))
- os.system("systemctl stop slapd")
- os.system(
- "rm -r /etc/ldap/slapd.d"
- ) # To be sure that we don't keep some part of the old config
- os.system("cp -r --preserve %s/ldap_config/. /etc/ldap/" % backup_folder)
- os.system("cp -r --preserve %s/ldap_db/. /var/lib/ldap/" % backup_folder)
- os.system(
- "cp -r --preserve %s/apps_settings/. /etc/yunohost/apps/"
- % backup_folder
- )
- os.system("systemctl start slapd")
- os.system("rm -r " + backup_folder)
- logger.info(m18n.n("migration_0019_rollback_success"))
- raise
- else:
- os.system("rm -r " + backup_folder)
+ # Migrate old settings
+ legacy_permission_settings = [
+ "skipped_uris",
+ "unprotected_uris",
+ "protected_uris",
+ "skipped_regex",
+ "unprotected_regex",
+ "protected_regex",
+ ]
+ if any(
+ app_setting(app_id, setting) is not None
+ for setting in legacy_permission_settings
+ ):
+ migrate_legacy_permission_settings(app=app_id)
diff --git a/src/yunohost/data_migrations/0020_ssh_sftp_permissions.py b/src/yunohost/data_migrations/0020_ssh_sftp_permissions.py
new file mode 100644
index 000000000..f1dbcd1e7
--- /dev/null
+++ b/src/yunohost/data_migrations/0020_ssh_sftp_permissions.py
@@ -0,0 +1,100 @@
+import subprocess
+import os
+
+from moulinette import m18n
+from moulinette.utils.log import getActionLogger
+
+from yunohost.tools import Migration
+from yunohost.permission import user_permission_update, permission_sync_to_user
+from yunohost.regenconf import manually_modified_files
+
+logger = getActionLogger("yunohost.migration")
+
+###################################################
+# Tools used also for restoration
+###################################################
+
+
+class MyMigration(Migration):
+ """
+ Add new permissions around SSH/SFTP features
+ """
+
+ introduced_in_version = "4.2.2"
+ dependencies = ["extend_permissions_features"]
+
+ @Migration.ldap_migration
+ def run(self, *args):
+
+ from yunohost.utils.ldap import _get_ldap_interface
+
+ ldap = _get_ldap_interface()
+
+ existing_perms_raw = ldap.search(
+ "ou=permission,dc=yunohost,dc=org", "(objectclass=permissionYnh)", ["cn"]
+ )
+ existing_perms = [perm["cn"][0] for perm in existing_perms_raw]
+
+ # Add SSH and SFTP permissions
+ if "sftp.main" not in existing_perms:
+ ldap.add(
+ "cn=sftp.main,ou=permission",
+ {
+ "cn": "sftp.main",
+ "gidNumber": "5004",
+ "objectClass": ["posixGroup", "permissionYnh"],
+ "groupPermission": [],
+ "authHeader": "FALSE",
+ "label": "SFTP",
+ "showTile": "FALSE",
+ "isProtected": "TRUE",
+ },
+ )
+
+ if "ssh.main" not in existing_perms:
+ ldap.add(
+ "cn=ssh.main,ou=permission",
+ {
+ "cn": "ssh.main",
+ "gidNumber": "5003",
+ "objectClass": ["posixGroup", "permissionYnh"],
+ "groupPermission": [],
+ "authHeader": "FALSE",
+ "label": "SSH",
+ "showTile": "FALSE",
+ "isProtected": "TRUE",
+ },
+ )
+
+ # Add a bash terminal to each users
+ users = ldap.search(
+ "ou=users,dc=yunohost,dc=org",
+ filter="(loginShell=*)",
+ attrs=["dn", "uid", "loginShell"],
+ )
+ for user in users:
+ if user["loginShell"][0] == "/bin/false":
+ dn = user["dn"][0].replace(",dc=yunohost,dc=org", "")
+ ldap.update(dn, {"loginShell": ["/bin/bash"]})
+ else:
+ user_permission_update(
+ "ssh.main", add=user["uid"][0], sync_perm=False
+ )
+
+ permission_sync_to_user()
+
+ # Somehow this is needed otherwise the PAM thing doesn't forget about the
+ # old loginShell value ?
+ subprocess.call(["nscd", "-i", "passwd"])
+
+ if (
+ "/etc/ssh/sshd_config" in manually_modified_files()
+ and os.system(
+ "grep -q '^ *AllowGroups\\|^ *AllowUsers' /etc/ssh/sshd_config"
+ )
+ != 0
+ ):
+ logger.error(m18n.n("diagnosis_sshd_config_insecure"))
+
+ def run_after_system_restore(self):
+ self.run()
diff --git a/src/yunohost/diagnosis.py b/src/yunohost/diagnosis.py
index b3e8b8636..4ac5e2731 100644
--- a/src/yunohost/diagnosis.py
+++ b/src/yunohost/diagnosis.py
@@ -28,7 +28,7 @@ import re
import os
import time
-from moulinette import m18n, msettings
+from moulinette import m18n, Moulinette
from moulinette.utils import log
from moulinette.utils.filesystem import (
read_json,
@@ -37,7 +37,7 @@ from moulinette.utils.filesystem import (
write_to_yaml,
)
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.hook import hook_list, hook_exec
logger = log.getActionLogger("yunohost.diagnosis")
@@ -59,11 +59,13 @@ def diagnosis_get(category, item):
all_categories_names = [c for c, _ in all_categories]
if category not in all_categories_names:
- raise YunohostError("diagnosis_unknown_categories", categories=category)
+ raise YunohostValidationError(
+ "diagnosis_unknown_categories", categories=category
+ )
if isinstance(item, list):
if any("=" not in criteria for criteria in item):
- raise YunohostError(
+ raise YunohostValidationError(
"Criterias should be of the form key=value (e.g. domain=yolo.test)"
)
@@ -91,7 +93,7 @@ def diagnosis_show(
else:
unknown_categories = [c for c in categories if c not in all_categories_names]
if unknown_categories:
- raise YunohostError(
+ raise YunohostValidationError(
"diagnosis_unknown_categories", categories=", ".join(unknown_categories)
)
@@ -136,7 +138,7 @@ def diagnosis_show(
url = yunopaste(content)
logger.info(m18n.n("log_available_on_yunopaste", url=url))
- if msettings.get("interface") == "api":
+ if Moulinette.interface.type == "api":
return {"url": url}
else:
return
@@ -181,7 +183,7 @@ def diagnosis_run(
else:
unknown_categories = [c for c in categories if c not in all_categories_names]
if unknown_categories:
- raise YunohostError(
+ raise YunohostValidationError(
"diagnosis_unknown_categories", categories=", ".join(unknown_categories)
)
@@ -217,11 +219,19 @@ def diagnosis_run(
if email:
_email_diagnosis_issues()
- if issues and msettings.get("interface") == "cli":
+ if issues and Moulinette.interface.type == "cli":
logger.warning(m18n.n("diagnosis_display_tip"))
-def diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
+def diagnosis_ignore(filter, list=False):
+ return _diagnosis_ignore(add_filter=filter, list=list)
+
+
+def diagnosis_unignore(filter):
+ return _diagnosis_ignore(remove_filter=filter)
+
+
+def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
"""
This action is meant for the admin to ignore issues reported by the
diagnosis system if they are known and understood by the admin. For
@@ -270,14 +280,14 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
# Sanity checks for the provided arguments
if len(filter_) == 0:
- raise YunohostError(
+ raise YunohostValidationError(
"You should provide at least one criteria being the diagnosis category to ignore"
)
category = filter_[0]
if category not in all_categories_names:
- raise YunohostError("%s is not a diagnosis category" % category)
+ raise YunohostValidationError("%s is not a diagnosis category" % category)
if any("=" not in criteria for criteria in filter_[1:]):
- raise YunohostError(
+ raise YunohostValidationError(
"Criterias should be of the form key=value (e.g. domain=yolo.test)"
)
@@ -331,7 +341,7 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
configuration["ignore_filters"][category] = []
if criterias not in configuration["ignore_filters"][category]:
- raise YunohostError("This filter does not exists.")
+ raise YunohostValidationError("This filter does not exists.")
configuration["ignore_filters"][category].remove(criterias)
_diagnosis_write_configuration(configuration)
@@ -551,9 +561,8 @@ class Diagnoser:
@staticmethod
def get_description(id_):
key = "diagnosis_description_" + id_
- descr = m18n.n(key)
# If no description available, fallback to id
- return descr if descr != key else id_
+ return m18n.n(key) if m18n.key_exists(key) else id_
@staticmethod
def i18n(report, force_remove_html_tags=False):
@@ -586,7 +595,7 @@ class Diagnoser:
info[1].update(meta_data)
s = m18n.n(info[0], **(info[1]))
# In cli, we remove the html tags
- if msettings.get("interface") != "api" or force_remove_html_tags:
+ if Moulinette.interface.type != "api" or force_remove_html_tags:
s = s.replace("", "'").replace(" ", "'")
s = html_tags.sub("", s.replace("
", "\n"))
else:
@@ -703,5 +712,5 @@ Subject: %s
import smtplib
smtp = smtplib.SMTP("localhost")
- smtp.sendmail(from_, [to_], message)
+ smtp.sendmail(from_, [to_], message.encode("utf-8"))
smtp.quit()
diff --git a/src/yunohost/dns.py b/src/yunohost/dns.py
new file mode 100644
index 000000000..534ade918
--- /dev/null
+++ b/src/yunohost/dns.py
@@ -0,0 +1,1015 @@
+# -*- coding: utf-8 -*-
+
+""" License
+
+ Copyright (C) 2013 YunoHost
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses
+
+"""
+
+""" yunohost_domain.py
+
+ Manage domains
+"""
+import os
+import re
+import time
+
+from difflib import SequenceMatcher
+from collections import OrderedDict
+
+from moulinette import m18n, Moulinette
+from moulinette.utils.log import getActionLogger
+from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir
+
+from yunohost.domain import (
+ domain_list,
+ _assert_domain_exists,
+ domain_config_get,
+ _get_domain_settings,
+ _set_domain_settings,
+ _list_subdomains_of,
+)
+from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld
+from yunohost.utils.error import YunohostValidationError, YunohostError
+from yunohost.utils.network import get_public_ip
+from yunohost.log import is_unit_operation
+from yunohost.hook import hook_callback
+
+logger = getActionLogger("yunohost.domain")
+
+DOMAIN_REGISTRAR_LIST_PATH = "/usr/share/yunohost/other/registrar_list.toml"
+
+
+def domain_dns_suggest(domain):
+ """
+ Generate DNS configuration for a domain
+
+ Keyword argument:
+ domain -- Domain name
+
+ """
+
+ if is_special_use_tld(domain):
+ return m18n.n("domain_dns_conf_special_use_tld")
+
+ _assert_domain_exists(domain)
+
+ dns_conf = _build_dns_conf(domain)
+
+ result = ""
+
+ if dns_conf["basic"]:
+ result += "; Basic ipv4/ipv6 records"
+ for record in dns_conf["basic"]:
+ result += "\n{name} {ttl} IN {type} {value}".format(**record)
+
+ if dns_conf["mail"]:
+ result += "\n\n"
+ result += "; Mail"
+ for record in dns_conf["mail"]:
+ result += "\n{name} {ttl} IN {type} {value}".format(**record)
+ result += "\n\n"
+
+ if dns_conf["xmpp"]:
+ result += "\n\n"
+ result += "; XMPP"
+ for record in dns_conf["xmpp"]:
+ result += "\n{name} {ttl} IN {type} {value}".format(**record)
+
+ if dns_conf["extra"]:
+ result += "; Extra"
+ for record in dns_conf["extra"]:
+ result += "\n{name} {ttl} IN {type} {value}".format(**record)
+
+ for name, record_list in dns_conf.items():
+ if name not in ("basic", "xmpp", "mail", "extra") and record_list:
+ result += "\n\n"
+ result += "; " + name
+ for record in record_list:
+ result += "\n{name} {ttl} IN {type} {value}".format(**record)
+
+ if Moulinette.interface.type == "cli":
+ # FIXME Update this to point to our "dns push" doc
+ logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation"))
+
+ return result
+
+
+def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
+ """
+ Internal function that will returns a data structure containing the needed
+ information to generate/adapt the dns configuration
+
+ Arguments:
+ domains -- List of a domain and its subdomains
+
+ The returned datastructure will have the following form:
+ {
+ "basic": [
+ # if ipv4 available
+ {"type": "A", "name": "@", "value": "123.123.123.123", "ttl": 3600},
+ # if ipv6 available
+ {"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600},
+ ],
+ "xmpp": [
+ {"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600},
+ {"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600},
+ {"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600},
+ {"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600},
+ {"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600}
+ {"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600}
+ ],
+ "mail": [
+ {"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600},
+ {"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 },
+ {"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600},
+ {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600}
+ ],
+ "extra": [
+ # if ipv4 available
+ {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600},
+ # if ipv6 available
+ {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600},
+ {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600},
+ ],
+ "example_of_a_custom_rule": [
+ {"type": "SRV", "name": "_matrix", "value": "domain.tld.", "ttl": 3600}
+ ],
+ }
+ """
+
+ basic = []
+ mail = []
+ xmpp = []
+ extra = []
+ ipv4 = get_public_ip()
+ ipv6 = get_public_ip(6)
+
+ # If this is a ynh_dyndns_domain, we're not gonna include all the subdomains in the conf
+ # Because dynette only accept a specific list of name/type
+ # And the wildcard */A already covers the bulk of use cases
+ if is_yunohost_dyndns_domain(base_domain):
+ subdomains = []
+ else:
+ subdomains = _list_subdomains_of(base_domain)
+
+ domains_settings = {
+ domain: domain_config_get(domain, export=True)
+ for domain in [base_domain] + subdomains
+ }
+
+ base_dns_zone = _get_dns_zone_for_domain(base_domain)
+
+ for domain, settings in domains_settings.items():
+
+ # Domain # Base DNS zone # Basename # Suffix #
+ # ------------------ # ----------------- # --------- # -------- #
+ # domain.tld # domain.tld # @ # #
+ # sub.domain.tld # domain.tld # sub # .sub #
+ # foo.sub.domain.tld # domain.tld # foo.sub # .foo.sub #
+ # sub.domain.tld # sub.domain.tld # @ # #
+ # foo.sub.domain.tld # sub.domain.tld # foo # .foo #
+
+ basename = domain.replace(base_dns_zone, "").rstrip(".") or "@"
+ suffix = f".{basename}" if basename != "@" else ""
+
+ # ttl = settings["ttl"]
+ ttl = 3600
+
+ ###########################
+ # Basic ipv4/ipv6 records #
+ ###########################
+ if ipv4:
+ basic.append([basename, ttl, "A", ipv4])
+
+ if ipv6:
+ basic.append([basename, ttl, "AAAA", ipv6])
+ elif include_empty_AAAA_if_no_ipv6:
+ basic.append([basename, ttl, "AAAA", None])
+
+ #########
+ # Email #
+ #########
+ if settings["mail_in"]:
+ mail.append([basename, ttl, "MX", f"10 {domain}."])
+
+ if settings["mail_out"]:
+ mail.append([basename, ttl, "TXT", '"v=spf1 a mx -all"'])
+
+ # DKIM/DMARC record
+ dkim_host, dkim_publickey = _get_DKIM(domain)
+
+ if dkim_host:
+ mail += [
+ [f"{dkim_host}{suffix}", ttl, "TXT", dkim_publickey],
+ [f"_dmarc{suffix}", ttl, "TXT", '"v=DMARC1; p=none"'],
+ ]
+
+ ########
+ # XMPP #
+ ########
+ if settings["xmpp"]:
+ xmpp += [
+ [
+ f"_xmpp-client._tcp{suffix}",
+ ttl,
+ "SRV",
+ f"0 5 5222 {domain}.",
+ ],
+ [
+ f"_xmpp-server._tcp{suffix}",
+ ttl,
+ "SRV",
+ f"0 5 5269 {domain}.",
+ ],
+ [f"muc{suffix}", ttl, "CNAME", basename],
+ [f"pubsub{suffix}", ttl, "CNAME", basename],
+ [f"vjud{suffix}", ttl, "CNAME", basename],
+ [f"xmpp-upload{suffix}", ttl, "CNAME", basename],
+ ]
+
+ #########
+ # Extra #
+ #########
+
+ # Only recommend wildcard and CAA for the top level
+ if domain == base_domain:
+ if ipv4:
+ extra.append([f"*{suffix}", ttl, "A", ipv4])
+
+ if ipv6:
+ extra.append([f"*{suffix}", ttl, "AAAA", ipv6])
+ elif include_empty_AAAA_if_no_ipv6:
+ extra.append([f"*{suffix}", ttl, "AAAA", None])
+
+ extra.append([basename, ttl, "CAA", '128 issue "letsencrypt.org"'])
+
+ ####################
+ # Standard records #
+ ####################
+
+ records = {
+ "basic": [
+ {"name": name, "ttl": ttl_, "type": type_, "value": value}
+ for name, ttl_, type_, value in basic
+ ],
+ "xmpp": [
+ {"name": name, "ttl": ttl_, "type": type_, "value": value}
+ for name, ttl_, type_, value in xmpp
+ ],
+ "mail": [
+ {"name": name, "ttl": ttl_, "type": type_, "value": value}
+ for name, ttl_, type_, value in mail
+ ],
+ "extra": [
+ {"name": name, "ttl": ttl_, "type": type_, "value": value}
+ for name, ttl_, type_, value in extra
+ ],
+ }
+
+ ##################
+ # Custom records #
+ ##################
+
+ # Defined by custom hooks ships in apps for example ...
+
+ # FIXME : this ain't practical for apps that may want to add
+ # custom dns records for a subdomain ... there's no easy way for
+ # an app to compare the base domain is the parent of the subdomain ?
+ # (On the other hand, in sep 2021, it looks like no app is using
+ # this mechanism...)
+
+ hook_results = hook_callback("custom_dns_rules", args=[base_domain])
+ for hook_name, results in hook_results.items():
+ #
+ # There can be multiple results per hook name, so results look like
+ # {'/some/path/to/hook1':
+ # { 'state': 'succeed',
+ # 'stdreturn': [{'type': 'SRV',
+ # 'name': 'stuff.foo.bar.',
+ # 'value': 'yoloswag',
+ # 'ttl': 3600}]
+ # },
+ # '/some/path/to/hook2':
+ # { ... },
+ # [...]
+ #
+ # Loop over the sub-results
+ custom_records = [
+ v["stdreturn"] for v in results.values() if v and v["stdreturn"]
+ ]
+
+ records[hook_name] = []
+ for record_list in custom_records:
+ # Check that record_list is indeed a list of dict
+ # with the required keys
+ if (
+ not isinstance(record_list, list)
+ or any(not isinstance(record, dict) for record in record_list)
+ or any(
+ key not in record
+ for record in record_list
+ for key in ["name", "ttl", "type", "value"]
+ )
+ ):
+ # Display an error, mainly for app packagers trying to implement a hook
+ logger.warning(
+ "Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s"
+ % (hook_name, record_list)
+ )
+ continue
+
+ records[hook_name].extend(record_list)
+
+ return records
+
+
+def _get_DKIM(domain):
+ DKIM_file = "/etc/dkim/{domain}.mail.txt".format(domain=domain)
+
+ if not os.path.isfile(DKIM_file):
+ return (None, None)
+
+ with open(DKIM_file) as f:
+ dkim_content = f.read()
+
+ # Gotta manage two formats :
+ #
+ # Legacy
+ # -----
+ #
+ # mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
+ # "p=" )
+ #
+ # New
+ # ------
+ #
+ # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; "
+ # "p=" )
+
+ is_legacy_format = " h=sha256; " not in dkim_content
+
+ # Legacy DKIM format
+ if is_legacy_format:
+ dkim = re.match(
+ (
+ r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+"
+ r'[^"]*"v=(?P[^";]+);'
+ r'[\s"]*k=(?P[^";]+);'
+ r'[\s"]*p=(?P[^";]+)'
+ ),
+ dkim_content,
+ re.M | re.S,
+ )
+ else:
+ dkim = re.match(
+ (
+ r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+"
+ r'[^"]*"v=(?P[^";]+);'
+ r'[\s"]*h=(?P[^";]+);'
+ r'[\s"]*k=(?P[^";]+);'
+ r'[\s"]*p=(?P[^";]+)'
+ ),
+ dkim_content,
+ re.M | re.S,
+ )
+
+ if not dkim:
+ return (None, None)
+
+ if is_legacy_format:
+ return (
+ dkim.group("host"),
+ '"v={v}; k={k}; p={p}"'.format(
+ v=dkim.group("v"), k=dkim.group("k"), p=dkim.group("p")
+ ),
+ )
+ else:
+ return (
+ dkim.group("host"),
+ '"v={v}; h={h}; k={k}; p={p}"'.format(
+ v=dkim.group("v"),
+ h=dkim.group("h"),
+ k=dkim.group("k"),
+ p=dkim.group("p"),
+ ),
+ )
+
+
+def _get_dns_zone_for_domain(domain):
+ """
+ Get the DNS zone of a domain
+
+ Keyword arguments:
+ domain -- The domain name
+
+ """
+
+ # First, check if domain is a nohost.me / noho.st / ynh.fr
+ # This is mainly meant to speed up things for "dyndns update"
+ # ... otherwise we end up constantly doing a bunch of dig requests
+ if is_yunohost_dyndns_domain(domain):
+ # Keep only foo.nohost.me even if we have subsub.sub.foo.nohost.me
+ return ".".join(domain.rsplit(".", 3)[-3:])
+
+ # Same thing with .local, .test, ... domains
+ if is_special_use_tld(domain):
+ # Keep only foo.local even if we have subsub.sub.foo.local
+ return ".".join(domain.rsplit(".", 2)[-2:])
+
+ # Check cache
+ cache_folder = "/var/cache/yunohost/dns_zones"
+ cache_file = f"{cache_folder}/{domain}"
+ cache_duration = 3600 # one hour
+ if (
+ os.path.exists(cache_file)
+ and abs(os.path.getctime(cache_file) - time.time()) < cache_duration
+ ):
+ dns_zone = read_file(cache_file).strip()
+ if dns_zone:
+ return dns_zone
+
+ # Check cache for parent domain
+ # This is another strick to try to prevent this function from being
+ # a bottleneck on system with 1 main domain + 10ish subdomains
+ # when building the dns conf for the main domain (which will call domain_config_get, etc...)
+ parent_domain = domain.split(".", 1)[1]
+ if parent_domain in domain_list()["domains"]:
+ parent_cache_file = f"{cache_folder}/{parent_domain}"
+ if (
+ os.path.exists(parent_cache_file)
+ and abs(os.path.getctime(parent_cache_file) - time.time()) < cache_duration
+ ):
+ dns_zone = read_file(parent_cache_file).strip()
+ if dns_zone:
+ return dns_zone
+
+ # For foo.bar.baz.gni we want to scan all the parent domains
+ # (including the domain itself)
+ # foo.bar.baz.gni
+ # bar.baz.gni
+ # baz.gni
+ # gni
+ # Until we find the first one that has a NS record
+ parent_list = [domain.split(".", i)[-1] for i, _ in enumerate(domain.split("."))]
+
+ for parent in parent_list:
+
+ # Check if there's a NS record for that domain
+ answer = dig(parent, rdtype="NS", full_answers=True, resolvers="force_external")
+ if answer[0] == "ok":
+ mkdir(cache_folder, parents=True, force=True)
+ write_to_file(cache_file, parent)
+ return parent
+
+ if len(parent_list) >= 2:
+ zone = parent_list[-2]
+ else:
+ zone = parent_list[-1]
+
+ logger.warning(
+ f"Could not identify the dns zone for domain {domain}, returning {zone}"
+ )
+ return zone
+
+
+def _get_registrar_config_section(domain):
+
+ from lexicon.providers.auto import _relevant_provider_for_domain
+
+ registrar_infos = {}
+
+ dns_zone = _get_dns_zone_for_domain(domain)
+
+ # If parent domain exists in yunohost
+ parent_domain = domain.split(".", 1)[1]
+ if parent_domain in domain_list()["domains"]:
+
+ # Dirty hack to have a link on the webadmin
+ if Moulinette.interface.type == "api":
+ parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/config)"
+ else:
+ parent_domain_link = parent_domain
+
+ registrar_infos["registrar"] = OrderedDict(
+ {
+ "type": "alert",
+ "style": "info",
+ "ask": m18n.n(
+ "domain_dns_registrar_managed_in_parent_domain",
+ parent_domain=domain,
+ parent_domain_link=parent_domain_link,
+ ),
+ "value": "parent_domain",
+ }
+ )
+ return OrderedDict(registrar_infos)
+
+ # TODO big project, integrate yunohost's dynette as a registrar-like provider
+ # TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron...
+ if is_yunohost_dyndns_domain(dns_zone):
+ registrar_infos["registrar"] = OrderedDict(
+ {
+ "type": "alert",
+ "style": "success",
+ "ask": m18n.n("domain_dns_registrar_yunohost"),
+ "value": "yunohost",
+ }
+ )
+ return OrderedDict(registrar_infos)
+ elif is_special_use_tld(dns_zone):
+ registrar_infos["registrar"] = OrderedDict(
+ {
+ "type": "alert",
+ "style": "info",
+ "ask": m18n.n("domain_dns_conf_special_use_tld"),
+ "value": None,
+ }
+ )
+
+ try:
+ registrar = _relevant_provider_for_domain(dns_zone)[0]
+ except ValueError:
+ registrar_infos["registrar"] = OrderedDict(
+ {
+ "type": "alert",
+ "style": "warning",
+ "ask": m18n.n("domain_dns_registrar_not_supported"),
+ "value": None,
+ }
+ )
+ else:
+
+ registrar_infos["registrar"] = OrderedDict(
+ {
+ "type": "alert",
+ "style": "info",
+ "ask": m18n.n("domain_dns_registrar_supported", registrar=registrar),
+ "value": registrar,
+ }
+ )
+
+ TESTED_REGISTRARS = ["ovh", "gandi"]
+ if registrar not in TESTED_REGISTRARS:
+ registrar_infos["experimental_disclaimer"] = OrderedDict(
+ {
+ "type": "alert",
+ "style": "danger",
+ "ask": m18n.n(
+ "domain_dns_registrar_experimental", registrar=registrar
+ ),
+ }
+ )
+
+ # TODO : add a help tip with the link to the registar's API doc (c.f. Lexicon's README)
+ registrar_list = read_toml(DOMAIN_REGISTRAR_LIST_PATH)
+ registrar_credentials = registrar_list[registrar]
+ for credential, infos in registrar_credentials.items():
+ infos["default"] = infos.get("default", "")
+ infos["optional"] = infos.get("optional", "False")
+ registrar_infos.update(registrar_credentials)
+
+ return OrderedDict(registrar_infos)
+
+
+def _get_registar_settings(domain):
+
+ _assert_domain_exists(domain)
+
+ settings = domain_config_get(domain, key="dns.registrar", export=True)
+
+ registrar = settings.pop("registrar")
+
+ if "experimental_disclaimer" in settings:
+ settings.pop("experimental_disclaimer")
+
+ return registrar, settings
+
+
+@is_unit_operation()
+def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=False):
+ """
+ Send DNS records to the previously-configured registrar of the domain.
+ """
+
+ from lexicon.client import Client as LexiconClient
+ from lexicon.config import ConfigResolver as LexiconConfigResolver
+
+ registrar, registrar_credentials = _get_registar_settings(domain)
+
+ _assert_domain_exists(domain)
+
+ if is_special_use_tld(domain):
+ logger.info(m18n.n("domain_dns_conf_special_use_tld"))
+ return {}
+
+ if not registrar or registrar == "None": # yes it's None as a string
+ raise YunohostValidationError("domain_dns_push_not_applicable", domain=domain)
+
+ # FIXME: in the future, properly unify this with yunohost dyndns update
+ if registrar == "yunohost":
+ logger.info(m18n.n("domain_dns_registrar_yunohost"))
+ return {}
+
+ if registrar == "parent_domain":
+ parent_domain = domain.split(".", 1)[1]
+ registar, registrar_credentials = _get_registar_settings(parent_domain)
+ if any(registrar_credentials.values()):
+ raise YunohostValidationError(
+ "domain_dns_push_managed_in_parent_domain",
+ domain=domain,
+ parent_domain=parent_domain,
+ )
+ else:
+ raise YunohostValidationError(
+ "domain_registrar_is_not_configured", domain=parent_domain
+ )
+
+ if not all(registrar_credentials.values()):
+ raise YunohostValidationError(
+ "domain_registrar_is_not_configured", domain=domain
+ )
+
+ base_dns_zone = _get_dns_zone_for_domain(domain)
+
+ # Convert the generated conf into a format that matches what we'll fetch using the API
+ # Makes it easier to compare "wanted records" with "current records on remote"
+ wanted_records = []
+ for records in _build_dns_conf(domain).values():
+ for record in records:
+
+ # Make sure the name is a FQDN
+ name = (
+ f"{record['name']}.{base_dns_zone}"
+ if record["name"] != "@"
+ else base_dns_zone
+ )
+ type_ = record["type"]
+ content = record["value"]
+
+ # Make sure the content is also a FQDN (with trailing . ?)
+ if content == "@" and record["type"] == "CNAME":
+ content = base_dns_zone + "."
+
+ wanted_records.append(
+ {"name": name, "type": type_, "ttl": record["ttl"], "content": content}
+ )
+
+ # FIXME Lexicon does not support CAA records
+ # See https://github.com/AnalogJ/lexicon/issues/282 and https://github.com/AnalogJ/lexicon/pull/371
+ # They say it's trivial to implement it!
+ # And yet, it is still not done/merged
+ # Update by Aleks: it works - at least with Gandi ?!
+ # wanted_records = [record for record in wanted_records if record["type"] != "CAA"]
+
+ if purge:
+ wanted_records = []
+ force = True
+
+ # Construct the base data structure to use lexicon's API.
+
+ base_config = {
+ "provider_name": registrar,
+ "domain": base_dns_zone,
+ registrar: registrar_credentials,
+ }
+
+ # Ugly hack to be able to fetch all record types at once:
+ # we initialize a LexiconClient with a dummy type "all"
+ # (which lexicon doesnt actually understands)
+ # then trigger ourselves the authentication + list_records
+ # instead of calling .execute()
+ query = (
+ LexiconConfigResolver()
+ .with_dict(dict_object=base_config)
+ .with_dict(dict_object={"action": "list", "type": "all"})
+ )
+ client = LexiconClient(query)
+ try:
+ client.provider.authenticate()
+ except Exception as e:
+ raise YunohostValidationError(
+ "domain_dns_push_failed_to_authenticate", domain=domain, error=str(e)
+ )
+
+ try:
+ current_records = client.provider.list_records()
+ except Exception as e:
+ raise YunohostError("domain_dns_push_failed_to_list", error=str(e))
+
+ managed_dns_records_hashes = _get_managed_dns_records_hashes(domain)
+
+ # Keep only records for relevant types: A, AAAA, MX, TXT, CNAME, SRV
+ relevant_types = ["A", "AAAA", "MX", "TXT", "CNAME", "SRV", "CAA"]
+ current_records = [r for r in current_records if r["type"] in relevant_types]
+
+ # Ignore records which are for a higher-level domain
+ # i.e. we don't care about the records for domain.tld when pushing yuno.domain.tld
+ current_records = [
+ r
+ for r in current_records
+ if r["name"].endswith(f".{domain}") or r["name"] == domain
+ ]
+
+ for record in current_records:
+
+ # Try to get rid of weird stuff like ".domain.tld" or "@.domain.tld"
+ record["name"] = record["name"].strip("@").strip(".")
+
+ # Some API return '@' in content and we shall convert it to absolute/fqdn
+ record["content"] = (
+ record["content"]
+ .replace("@.", base_dns_zone + ".")
+ .replace("@", base_dns_zone + ".")
+ )
+
+ if record["type"] == "TXT":
+ if not record["content"].startswith('"'):
+ record["content"] = '"' + record["content"]
+ if not record["content"].endswith('"'):
+ record["content"] = record["content"] + '"'
+
+ # Check if this record was previously set by YunoHost
+ record["managed_by_yunohost"] = (
+ _hash_dns_record(record) in managed_dns_records_hashes
+ )
+
+ # Step 0 : Get the list of unique (type, name)
+ # And compare the current and wanted records
+ #
+ # i.e. we want this kind of stuff:
+ # wanted current
+ # (A, .domain.tld) 1.2.3.4 1.2.3.4
+ # (A, www.domain.tld) 1.2.3.4 5.6.7.8
+ # (A, foobar.domain.tld) 1.2.3.4
+ # (AAAA, .domain.tld) 2001::abcd
+ # (MX, .domain.tld) 10 domain.tld [10 mx1.ovh.net, 20 mx2.ovh.net]
+ # (TXT, .domain.tld) "v=spf1 ..." ["v=spf1", "foobar"]
+ # (SRV, .domain.tld) 0 5 5269 domain.tld
+ changes = {"delete": [], "update": [], "create": [], "unchanged": []}
+
+ type_and_names = sorted(
+ set([(r["type"], r["name"]) for r in current_records + wanted_records])
+ )
+ comparison = {
+ type_and_name: {"current": [], "wanted": []} for type_and_name in type_and_names
+ }
+
+ for record in current_records:
+ comparison[(record["type"], record["name"])]["current"].append(record)
+
+ for record in wanted_records:
+ comparison[(record["type"], record["name"])]["wanted"].append(record)
+
+ for type_and_name, records in comparison.items():
+
+ #
+ # Step 1 : compute a first "diff" where we remove records which are the same on both sides
+ #
+ wanted_contents = [r["content"] for r in records["wanted"]]
+ current_contents = [r["content"] for r in records["current"]]
+
+ current = [r for r in records["current"] if r["content"] not in wanted_contents]
+ wanted = [r for r in records["wanted"] if r["content"] not in current_contents]
+
+ #
+ # Step 2 : simple case: 0 record on one side, 0 on the other
+ # -> either nothing do (0/0) or creations (0/N) or deletions (N/0)
+ #
+ if len(current) == 0 and len(wanted) == 0:
+ # No diff, nothing to do
+ changes["unchanged"].extend(records["current"])
+ continue
+
+ elif len(wanted) == 0:
+ changes["delete"].extend(current)
+ continue
+
+ elif len(current) == 0:
+ changes["create"].extend(wanted)
+ continue
+
+ #
+ # Step 3 : N record on one side, M on the other
+ #
+ # Fuzzy matching strategy:
+ # For each wanted record, try to find a current record which looks like the wanted one
+ # -> if found, trigger an update
+ # -> if no match found, trigger a create
+ #
+ for record in wanted:
+
+ def likeliness(r):
+ # We compute this only on the first 100 chars, to have a high value even for completely different DKIM keys
+ return SequenceMatcher(
+ None, r["content"][:100], record["content"][:100]
+ ).ratio()
+
+ matches = sorted(current, key=lambda r: likeliness(r), reverse=True)
+ if matches and likeliness(matches[0]) > 0.50:
+ match = matches[0]
+ # Remove the match from 'current' so that it's not added to the removed stuff later
+ current.remove(match)
+ match["old_content"] = match["content"]
+ match["content"] = record["content"]
+ changes["update"].append(match)
+ else:
+ changes["create"].append(record)
+
+ #
+ # For all other remaining current records:
+ # -> trigger deletions
+ #
+ for record in current:
+ changes["delete"].append(record)
+
+ def relative_name(name):
+ name = name.strip(".")
+ name = name.replace("." + base_dns_zone, "")
+ name = name.replace(base_dns_zone, "@")
+ return name
+
+ def human_readable_record(action, record):
+ name = relative_name(record["name"])
+ name = name[:20]
+ t = record["type"]
+
+ if not force and action in ["update", "delete"]:
+ ignored = (
+ ""
+ if record["managed_by_yunohost"]
+ else "(ignored, won't be changed by Yunohost unless forced)"
+ )
+ else:
+ ignored = ""
+
+ if action == "create":
+ old_content = record.get("old_content", "(None)")[:30]
+ new_content = record.get("content", "(None)")[:30]
+ return f"{name:>20} [{t:^5}] {new_content:^30} {ignored}"
+ elif action == "update":
+ old_content = record.get("old_content", "(None)")[:30]
+ new_content = record.get("content", "(None)")[:30]
+ return (
+ f"{name:>20} [{t:^5}] {old_content:^30} -> {new_content:^30} {ignored}"
+ )
+ elif action == "unchanged":
+ old_content = new_content = record.get("content", "(None)")[:30]
+ return f"{name:>20} [{t:^5}] {old_content:^30}"
+ else:
+ old_content = record.get("content", "(None)")[:30]
+ return f"{name:>20} [{t:^5}] {old_content:^30} {ignored}"
+
+ if dry_run:
+ if Moulinette.interface.type == "api":
+ for records in changes.values():
+ for record in records:
+ record["name"] = relative_name(record["name"])
+ return changes
+ else:
+ out = {"delete": [], "create": [], "update": [], "unchanged": []}
+ for action in ["delete", "create", "update", "unchanged"]:
+ for record in changes[action]:
+ out[action].append(human_readable_record(action, record))
+
+ return out
+
+ # If --force ain't used, we won't delete/update records not managed by yunohost
+ if not force:
+ for action in ["delete", "update"]:
+ changes[action] = [r for r in changes[action] if r["managed_by_yunohost"]]
+
+ def progress(info=""):
+ progress.nb += 1
+ width = 20
+ bar = int(progress.nb * width / progress.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 = ""
+ progress.total = len(changes["delete"] + changes["create"] + changes["update"])
+
+ if progress.total == 0:
+ logger.success(m18n.n("domain_dns_push_already_up_to_date"))
+ return {}
+
+ #
+ # Actually push the records
+ #
+
+ operation_logger.start()
+ logger.info(m18n.n("domain_dns_pushing"))
+
+ new_managed_dns_records_hashes = [_hash_dns_record(r) for r in changes["unchanged"]]
+ results = {"warnings": [], "errors": []}
+
+ for action in ["delete", "create", "update"]:
+
+ for record in changes[action]:
+
+ relative_name = record["name"].replace(base_dns_zone, "").rstrip(".") or "@"
+ progress(
+ f"{action} {record['type']:^5} / {relative_name}"
+ ) # FIXME: i18n but meh
+
+ # Apparently Lexicon yields us some 'id' during fetch
+ # But wants 'identifier' during push ...
+ if "id" in record:
+ record["identifier"] = record["id"]
+ del record["id"]
+
+ if registrar == "godaddy":
+ if record["name"] == base_dns_zone:
+ record["name"] = "@." + record["name"]
+ if record["type"] in ["MX", "SRV", "CAA"]:
+ logger.warning(
+ f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy."
+ )
+ results["warnings"].append(
+ f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy."
+ )
+ continue
+
+ record["action"] = action
+ query = (
+ LexiconConfigResolver()
+ .with_dict(dict_object=base_config)
+ .with_dict(dict_object=record)
+ )
+
+ try:
+ result = LexiconClient(query).execute()
+ except Exception as e:
+ msg = m18n.n(
+ "domain_dns_push_record_failed",
+ action=action,
+ type=record["type"],
+ name=record["name"],
+ error=str(e),
+ )
+ logger.error(msg)
+ results["errors"].append(msg)
+ else:
+ if result:
+ new_managed_dns_records_hashes.append(_hash_dns_record(record))
+ else:
+ msg = m18n.n(
+ "domain_dns_push_record_failed",
+ action=action,
+ type=record["type"],
+ name=record["name"],
+ error="unkonwn error?",
+ )
+ logger.error(msg)
+ results["errors"].append(msg)
+
+ _set_managed_dns_records_hashes(domain, new_managed_dns_records_hashes)
+
+ # Everything succeeded
+ if len(results["errors"]) + len(results["warnings"]) == 0:
+ logger.success(m18n.n("domain_dns_push_success"))
+ return {}
+ # Everything failed
+ elif len(results["errors"]) + len(results["warnings"]) == progress.total:
+ logger.error(m18n.n("domain_dns_push_failed"))
+ else:
+ logger.warning(m18n.n("domain_dns_push_partial_failure"))
+
+ return results
+
+
+def _get_managed_dns_records_hashes(domain: str) -> list:
+ return _get_domain_settings(domain).get("managed_dns_records_hashes", [])
+
+
+def _set_managed_dns_records_hashes(domain: str, hashes: list) -> None:
+ settings = _get_domain_settings(domain)
+ settings["managed_dns_records_hashes"] = hashes or []
+ _set_domain_settings(domain, settings)
+
+
+def _hash_dns_record(record: dict) -> int:
+
+ fields = ["name", "type", "content"]
+ record_ = {f: record.get(f) for f in fields}
+
+ return hash(frozenset(record_.items()))
diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py
index f28753311..92e52cd05 100644
--- a/src/yunohost/domain.py
+++ b/src/yunohost/domain.py
@@ -24,13 +24,12 @@
Manage domains
"""
import os
-import re
+from typing import Dict, Any
-from moulinette import m18n, msettings
+from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError
-from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
-from moulinette.utils.filesystem import write_to_file
+from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml, rm
from yunohost.app import (
app_ssowatconf,
@@ -39,12 +38,18 @@ from yunohost.app import (
_get_conflicting_apps,
)
from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf
-from yunohost.utils.network import get_public_ip
+from yunohost.utils.config import ConfigPanel, Question
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.log import is_unit_operation
-from yunohost.hook import hook_callback
logger = getActionLogger("yunohost.domain")
+DOMAIN_CONFIG_PATH = "/usr/share/yunohost/other/config_domain.toml"
+DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains"
+
+# Lazy dev caching to avoid re-query ldap every time we need the domain list
+domain_list_cache: Dict[str, Any] = {}
+
def domain_list(exclude_subdomains=False):
"""
@@ -54,6 +59,10 @@ def domain_list(exclude_subdomains=False):
exclude_subdomains -- Filter out domains that are subdomains of other declared domains
"""
+ global domain_list_cache
+ if not exclude_subdomains and domain_list_cache:
+ return domain_list_cache
+
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
@@ -83,7 +92,44 @@ def domain_list(exclude_subdomains=False):
result_list = sorted(result_list, key=cmp_domain)
- return {"domains": result_list, "main": _get_maindomain()}
+ # Don't cache answer if using exclude_subdomains
+ if exclude_subdomains:
+ return {"domains": result_list, "main": _get_maindomain()}
+
+ domain_list_cache = {"domains": result_list, "main": _get_maindomain()}
+ return domain_list_cache
+
+
+def _assert_domain_exists(domain):
+ if domain not in domain_list()["domains"]:
+ raise YunohostValidationError("domain_name_unknown", domain=domain)
+
+
+def _list_subdomains_of(parent_domain):
+
+ _assert_domain_exists(parent_domain)
+
+ out = []
+ for domain in domain_list()["domains"]:
+ if domain.endswith(f".{parent_domain}"):
+ out.append(domain)
+
+ return out
+
+
+def _get_parent_domain_of(domain):
+
+ _assert_domain_exists(domain)
+
+ if "." not in domain:
+ return domain
+
+ parent_domain = domain.split(".", 1)[-1]
+ if parent_domain not in domain_list()["domains"]:
+ return domain # Domain is its own parent
+
+ else:
+ return _get_parent_domain_of(parent_domain)
@is_unit_operation()
@@ -99,9 +145,10 @@ def domain_add(operation_logger, domain, dyndns=False):
from yunohost.hook import hook_callback
from yunohost.app import app_ssowatconf
from yunohost.utils.ldap import _get_ldap_interface
+ from yunohost.certificate import _certificate_install_selfsigned
if domain.startswith("xmpp-upload."):
- raise YunohostError("domain_cannot_add_xmpp_upload")
+ raise YunohostValidationError("domain_cannot_add_xmpp_upload")
if domain.startswith("muc."):
raise YunohostError("domain_cannot_add_muc_upload")
@@ -111,36 +158,40 @@ def domain_add(operation_logger, domain, dyndns=False):
try:
ldap.validate_uniqueness({"virtualdomain": domain})
except MoulinetteError:
- raise YunohostError("domain_exists")
-
- operation_logger.start()
+ raise YunohostValidationError("domain_exists")
# Lower domain to avoid some edge cases issues
# See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765
domain = domain.lower()
+ # Non-latin characters (e.g. café.com => xn--caf-dma.com)
+ domain = domain.encode("idna").decode("utf-8")
+
# DynDNS domain
if dyndns:
- # Do not allow to subscribe to multiple dyndns domains...
- if os.path.exists("/etc/cron.d/yunohost-dyndns"):
- raise YunohostError("domain_dyndns_already_subscribed")
+ from yunohost.dyndns import _dyndns_provides, _guess_current_dyndns_domain
- from yunohost.dyndns import dyndns_subscribe, _dyndns_provides
+ # Do not allow to subscribe to multiple dyndns domains...
+ if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None):
+ raise YunohostValidationError("domain_dyndns_already_subscribed")
# Check that this domain can effectively be provided by
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
if not _dyndns_provides("dyndns.yunohost.org", domain):
- raise YunohostError("domain_dyndns_root_unknown")
+ raise YunohostValidationError("domain_dyndns_root_unknown")
+
+ operation_logger.start()
+
+ if dyndns:
+ from yunohost.dyndns import dyndns_subscribe
# Actually subscribe
dyndns_subscribe(domain=domain)
+ _certificate_install_selfsigned([domain], False)
+
try:
- import yunohost.certificate
-
- yunohost.certificate._certificate_install_selfsigned([domain], False)
-
attr_dict = {
"objectClass": ["mailDomain", "top"],
"virtualdomain": domain,
@@ -150,6 +201,9 @@ def domain_add(operation_logger, domain, dyndns=False):
ldap.add("virtualdomain=%s,ou=domains" % domain, attr_dict)
except Exception as e:
raise YunohostError("domain_creation_failed", domain=domain, error=e)
+ finally:
+ global domain_list_cache
+ domain_list_cache = {}
# Don't regen these conf if we're still in postinstall
if os.path.exists("/etc/yunohost/installed"):
@@ -164,16 +218,18 @@ def domain_add(operation_logger, domain, dyndns=False):
# because it's one of the major service, but in the long term we
# should identify the root of this bug...
_force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain])
- regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd"])
+ regen_conf(
+ names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"]
+ )
app_ssowatconf()
- except Exception:
+ except Exception as e:
# Force domain removal silently
try:
- domain_remove(domain, True)
+ domain_remove(domain, force=True)
except Exception:
pass
- raise
+ raise e
hook_callback("post_domain_add", args=[domain])
@@ -181,21 +237,26 @@ def domain_add(operation_logger, domain, dyndns=False):
@is_unit_operation()
-def domain_remove(operation_logger, domain, force=False):
+def domain_remove(operation_logger, domain, remove_apps=False, force=False):
"""
Delete domains
Keyword argument:
domain -- Domain to delete
- force -- Force the domain removal
+ remove_apps -- Remove applications installed on the domain
+ force -- Force the domain removal and don't not ask confirmation to
+ remove apps if remove_apps is specified
"""
from yunohost.hook import hook_callback
- from yunohost.app import app_ssowatconf, app_info
+ from yunohost.app import app_ssowatconf, app_info, app_remove
from yunohost.utils.ldap import _get_ldap_interface
- if not force and domain not in domain_list()["domains"]:
- raise YunohostError("domain_name_unknown", domain=domain)
+ # the 'force' here is related to the exception happening in domain_add ...
+ # we don't want to check the domain exists because the ldap add may have
+ # failed
+ if not force:
+ _assert_domain_exists(domain)
# Check domain is not the main domain
if domain == _get_maindomain():
@@ -203,13 +264,15 @@ def domain_remove(operation_logger, domain, force=False):
other_domains.remove(domain)
if other_domains:
- raise YunohostError(
+ raise YunohostValidationError(
"domain_cannot_remove_main",
domain=domain,
other_domains="\n * " + ("\n * ".join(other_domains)),
)
else:
- raise YunohostError("domain_cannot_remove_main_add_new_one", domain=domain)
+ raise YunohostValidationError(
+ "domain_cannot_remove_main_add_new_one", domain=domain
+ )
# Check if apps are installed on the domain
apps_on_that_domain = []
@@ -219,24 +282,56 @@ def domain_remove(operation_logger, domain, force=False):
label = app_info(app)["name"]
if settings.get("domain") == domain:
apps_on_that_domain.append(
- ' - %s "%s" on https://%s%s' % (app, label, domain, settings["path"])
- if "path" in settings
- else app
+ (
+ app,
+ ' - %s "%s" on https://%s%s'
+ % (app, label, domain, settings["path"])
+ if "path" in settings
+ else app,
+ )
)
if apps_on_that_domain:
- raise YunohostError(
- "domain_uninstall_app_first", apps="\n".join(apps_on_that_domain)
- )
+ if remove_apps:
+ if Moulinette.interface.type == "cli" and not force:
+ answer = Moulinette.prompt(
+ m18n.n(
+ "domain_remove_confirm_apps_removal",
+ apps="\n".join([x[1] for x in apps_on_that_domain]),
+ answers="y/N",
+ ),
+ color="yellow",
+ )
+ if answer.upper() != "Y":
+ raise YunohostError("aborting")
+
+ for app, _ in apps_on_that_domain:
+ app_remove(app)
+ else:
+ raise YunohostValidationError(
+ "domain_uninstall_app_first",
+ apps="\n".join([x[1] for x in apps_on_that_domain]),
+ )
operation_logger.start()
+
ldap = _get_ldap_interface()
try:
ldap.remove("virtualdomain=" + domain + ",ou=domains")
except Exception as e:
raise YunohostError("domain_deletion_failed", domain=domain, error=e)
+ finally:
+ global domain_list_cache
+ domain_list_cache = {}
- os.system("rm -rf /etc/yunohost/certs/%s" % domain)
+ stuff_to_delete = [
+ f"/etc/yunohost/certs/{domain}",
+ f"/etc/yunohost/dyndns/K{domain}.+*",
+ f"{DOMAIN_SETTINGS_DIR}/{domain}.yml",
+ ]
+
+ for stuff in stuff_to_delete:
+ rm(stuff, force=True, recursive=True)
# Sometime we have weird issues with the regenconf where some files
# appears as manually modified even though they weren't touched ...
@@ -259,7 +354,7 @@ def domain_remove(operation_logger, domain, force=False):
"/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True
)
- regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix"])
+ regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"])
app_ssowatconf()
hook_callback("post_domain_remove", args=[domain])
@@ -267,57 +362,6 @@ def domain_remove(operation_logger, domain, force=False):
logger.success(m18n.n("domain_deleted"))
-def domain_dns_conf(domain, ttl=None):
- """
- Generate DNS configuration for a domain
-
- Keyword argument:
- domain -- Domain name
- ttl -- Time to live
-
- """
-
- if domain not in domain_list()["domains"]:
- raise YunohostError("domain_name_unknown", domain=domain)
-
- ttl = 3600 if ttl is None else ttl
-
- dns_conf = _build_dns_conf(domain, ttl)
-
- result = ""
-
- result += "; Basic ipv4/ipv6 records"
- for record in dns_conf["basic"]:
- result += "\n{name} {ttl} IN {type} {value}".format(**record)
-
- result += "\n\n"
- result += "; XMPP"
- for record in dns_conf["xmpp"]:
- result += "\n{name} {ttl} IN {type} {value}".format(**record)
-
- result += "\n\n"
- result += "; Mail"
- for record in dns_conf["mail"]:
- result += "\n{name} {ttl} IN {type} {value}".format(**record)
- result += "\n\n"
-
- result += "; Extra"
- for record in dns_conf["extra"]:
- result += "\n{name} {ttl} IN {type} {value}".format(**record)
-
- for name, record_list in dns_conf.items():
- if name not in ("basic", "xmpp", "mail", "extra") and record_list:
- result += "\n\n"
- result += "; " + name
- for record in record_list:
- result += "\n{name} {ttl} IN {type} {value}".format(**record)
-
- if msettings.get("interface") == "cli":
- logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation"))
-
- return result
-
-
@is_unit_operation()
def domain_main_domain(operation_logger, new_main_domain=None):
"""
@@ -334,8 +378,7 @@ def domain_main_domain(operation_logger, new_main_domain=None):
return {"current_main_domain": _get_maindomain()}
# Check domain exists
- if new_main_domain not in domain_list()["domains"]:
- raise YunohostError("domain_name_unknown", domain=new_main_domain)
+ _assert_domain_exists(new_main_domain)
operation_logger.related_to.append(("domain", new_main_domain))
operation_logger.start()
@@ -343,7 +386,8 @@ def domain_main_domain(operation_logger, new_main_domain=None):
# Apply changes to ssl certs
try:
write_to_file("/etc/yunohost/current_host", new_main_domain)
-
+ global domain_list_cache
+ domain_list_cache = {}
_set_hostname(new_main_domain)
except Exception as e:
logger.warning("%s" % e, exc_info=1)
@@ -359,6 +403,116 @@ def domain_main_domain(operation_logger, new_main_domain=None):
logger.success(m18n.n("main_domain_changed"))
+def domain_url_available(domain, path):
+ """
+ Check availability of a web path
+
+ Keyword argument:
+ domain -- The domain for the web path (e.g. your.domain.tld)
+ path -- The path to check (e.g. /coffee)
+ """
+
+ return len(_get_conflicting_apps(domain, path)) == 0
+
+
+def _get_maindomain():
+ with open("/etc/yunohost/current_host", "r") as f:
+ maindomain = f.readline().rstrip()
+ return maindomain
+
+
+def domain_config_get(domain, key="", full=False, export=False):
+ """
+ Display a domain configuration
+ """
+
+ if full and export:
+ raise YunohostValidationError(
+ "You can't use --full and --export together.", raw_msg=True
+ )
+
+ if full:
+ mode = "full"
+ elif export:
+ mode = "export"
+ else:
+ mode = "classic"
+
+ config = DomainConfigPanel(domain)
+ return config.get(key, mode)
+
+
+@is_unit_operation()
+def domain_config_set(
+ operation_logger, domain, key=None, value=None, args=None, args_file=None
+):
+ """
+ Apply a new domain configuration
+ """
+ Question.operation_logger = operation_logger
+ config = DomainConfigPanel(domain)
+ return config.set(key, value, args, args_file, operation_logger=operation_logger)
+
+
+class DomainConfigPanel(ConfigPanel):
+ def __init__(self, domain):
+ _assert_domain_exists(domain)
+ self.domain = domain
+ self.save_mode = "diff"
+ super().__init__(
+ config_path=DOMAIN_CONFIG_PATH,
+ save_path=f"{DOMAIN_SETTINGS_DIR}/{domain}.yml",
+ )
+
+ def _get_toml(self):
+ from yunohost.dns import _get_registrar_config_section
+
+ toml = super()._get_toml()
+
+ toml["feature"]["xmpp"]["xmpp"]["default"] = (
+ 1 if self.domain == _get_maindomain() else 0
+ )
+ toml["dns"]["registrar"] = _get_registrar_config_section(self.domain)
+
+ # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ...
+ self.registar_id = toml["dns"]["registrar"]["registrar"]["value"]
+ del toml["dns"]["registrar"]["registrar"]["value"]
+
+ return toml
+
+ def _load_current_values(self):
+
+ # TODO add mechanism to share some settings with other domains on the same zone
+ super()._load_current_values()
+
+ # FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ...
+ self.values["registrar"] = self.registar_id
+
+
+def _get_domain_settings(domain: str) -> dict:
+
+ _assert_domain_exists(domain)
+
+ if os.path.exists(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml"):
+ return read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml") or {}
+ else:
+ return {}
+
+
+def _set_domain_settings(domain: str, settings: dict) -> None:
+
+ _assert_domain_exists(domain)
+
+ write_to_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", settings)
+
+
+#
+#
+# Stuff managed in other files
+#
+#
+
+
def domain_cert_status(domain_list, full=False):
import yunohost.certificate
@@ -385,268 +539,17 @@ def domain_cert_renew(
)
-def domain_url_available(domain, path):
- """
- Check availability of a web path
-
- Keyword argument:
- domain -- The domain for the web path (e.g. your.domain.tld)
- path -- The path to check (e.g. /coffee)
- """
-
- return len(_get_conflicting_apps(domain, path)) == 0
+def domain_dns_conf(domain):
+ return domain_dns_suggest(domain)
-def _get_maindomain():
- with open("/etc/yunohost/current_host", "r") as f:
- maindomain = f.readline().rstrip()
- return maindomain
+def domain_dns_suggest(domain):
+ import yunohost.dns
+
+ return yunohost.dns.domain_dns_suggest(domain)
-def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False):
- """
- Internal function that will returns a data structure containing the needed
- information to generate/adapt the dns configuration
+def domain_dns_push(domain, dry_run, force, purge):
+ import yunohost.dns
- The returned datastructure will have the following form:
- {
- "basic": [
- # if ipv4 available
- {"type": "A", "name": "@", "value": "123.123.123.123", "ttl": 3600},
- # if ipv6 available
- {"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600},
- ],
- "xmpp": [
- {"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600},
- {"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600},
- {"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600},
- {"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600},
- {"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600}
- {"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600}
- ],
- "mail": [
- {"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600},
- {"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 },
- {"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600},
- {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600}
- ],
- "extra": [
- # if ipv4 available
- {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600},
- # if ipv6 available
- {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600},
- {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600},
- ],
- "example_of_a_custom_rule": [
- {"type": "SRV", "name": "_matrix", "value": "domain.tld.", "ttl": 3600}
- ],
- }
- """
-
- ipv4 = get_public_ip()
- ipv6 = get_public_ip(6)
-
- ###########################
- # Basic ipv4/ipv6 records #
- ###########################
-
- basic = []
- if ipv4:
- basic.append(["@", ttl, "A", ipv4])
-
- if ipv6:
- basic.append(["@", ttl, "AAAA", ipv6])
- elif include_empty_AAAA_if_no_ipv6:
- basic.append(["@", ttl, "AAAA", None])
-
- #########
- # Email #
- #########
-
- mail = [
- ["@", ttl, "MX", "10 %s." % domain],
- ["@", ttl, "TXT", '"v=spf1 a mx -all"'],
- ]
-
- # DKIM/DMARC record
- dkim_host, dkim_publickey = _get_DKIM(domain)
-
- if dkim_host:
- mail += [
- [dkim_host, ttl, "TXT", dkim_publickey],
- ["_dmarc", ttl, "TXT", '"v=DMARC1; p=none"'],
- ]
-
- ########
- # XMPP #
- ########
-
- xmpp = [
- ["_xmpp-client._tcp", ttl, "SRV", "0 5 5222 %s." % domain],
- ["_xmpp-server._tcp", ttl, "SRV", "0 5 5269 %s." % domain],
- ["muc", ttl, "CNAME", "@"],
- ["pubsub", ttl, "CNAME", "@"],
- ["vjud", ttl, "CNAME", "@"],
- ["xmpp-upload", ttl, "CNAME", "@"],
- ]
-
- #########
- # Extra #
- #########
-
- extra = []
-
- if ipv4:
- extra.append(["*", ttl, "A", ipv4])
-
- if ipv6:
- extra.append(["*", ttl, "AAAA", ipv6])
- elif include_empty_AAAA_if_no_ipv6:
- extra.append(["*", ttl, "AAAA", None])
-
- extra.append(["@", ttl, "CAA", '128 issue "letsencrypt.org"'])
-
- ####################
- # Standard records #
- ####################
-
- records = {
- "basic": [
- {"name": name, "ttl": ttl_, "type": type_, "value": value}
- for name, ttl_, type_, value in basic
- ],
- "xmpp": [
- {"name": name, "ttl": ttl_, "type": type_, "value": value}
- for name, ttl_, type_, value in xmpp
- ],
- "mail": [
- {"name": name, "ttl": ttl_, "type": type_, "value": value}
- for name, ttl_, type_, value in mail
- ],
- "extra": [
- {"name": name, "ttl": ttl_, "type": type_, "value": value}
- for name, ttl_, type_, value in extra
- ],
- }
-
- ##################
- # Custom records #
- ##################
-
- # Defined by custom hooks ships in apps for example ...
-
- hook_results = hook_callback("custom_dns_rules", args=[domain])
- for hook_name, results in hook_results.items():
- #
- # There can be multiple results per hook name, so results look like
- # {'/some/path/to/hook1':
- # { 'state': 'succeed',
- # 'stdreturn': [{'type': 'SRV',
- # 'name': 'stuff.foo.bar.',
- # 'value': 'yoloswag',
- # 'ttl': 3600}]
- # },
- # '/some/path/to/hook2':
- # { ... },
- # [...]
- #
- # Loop over the sub-results
- custom_records = [
- v["stdreturn"] for v in results.values() if v and v["stdreturn"]
- ]
-
- records[hook_name] = []
- for record_list in custom_records:
- # Check that record_list is indeed a list of dict
- # with the required keys
- if (
- not isinstance(record_list, list)
- or any(not isinstance(record, dict) for record in record_list)
- or any(
- key not in record
- for record in record_list
- for key in ["name", "ttl", "type", "value"]
- )
- ):
- # Display an error, mainly for app packagers trying to implement a hook
- logger.warning(
- "Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s"
- % (hook_name, record_list)
- )
- continue
-
- records[hook_name].extend(record_list)
-
- return records
-
-
-def _get_DKIM(domain):
- DKIM_file = "/etc/dkim/{domain}.mail.txt".format(domain=domain)
-
- if not os.path.isfile(DKIM_file):
- return (None, None)
-
- with open(DKIM_file) as f:
- dkim_content = f.read()
-
- # Gotta manage two formats :
- #
- # Legacy
- # -----
- #
- # mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
- # "p=" )
- #
- # New
- # ------
- #
- # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; "
- # "p=" )
-
- is_legacy_format = " h=sha256; " not in dkim_content
-
- # Legacy DKIM format
- if is_legacy_format:
- dkim = re.match(
- (
- r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+"
- r'[^"]*"v=(?P[^";]+);'
- r'[\s"]*k=(?P[^";]+);'
- r'[\s"]*p=(?P[^";]+)'
- ),
- dkim_content,
- re.M | re.S,
- )
- else:
- dkim = re.match(
- (
- r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+"
- r'[^"]*"v=(?P[^";]+);'
- r'[\s"]*h=(?P[^";]+);'
- r'[\s"]*k=(?P[^";]+);'
- r'[\s"]*p=(?P[^";]+)'
- ),
- dkim_content,
- re.M | re.S,
- )
-
- if not dkim:
- return (None, None)
-
- if is_legacy_format:
- return (
- dkim.group("host"),
- '"v={v}; k={k}; p={p}"'.format(
- v=dkim.group("v"), k=dkim.group("k"), p=dkim.group("p")
- ),
- )
- else:
- return (
- dkim.group("host"),
- '"v={v}; h={h}; k={k}; p={p}"'.format(
- v=dkim.group("v"),
- h=dkim.group("h"),
- k=dkim.group("k"),
- p=dkim.group("p"),
- ),
- )
+ return yunohost.dns.domain_dns_push(domain, dry_run, force, purge)
diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py
index d94748881..e33cf4f22 100644
--- a/src/yunohost/dyndns.py
+++ b/src/yunohost/dyndns.py
@@ -33,13 +33,15 @@ import subprocess
from moulinette import m18n
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
-from moulinette.utils.filesystem import write_to_file, read_file
+from moulinette.utils.filesystem import write_to_file, read_file, rm, chown, chmod
from moulinette.utils.network import download_json
-from yunohost.utils.error import YunohostError
-from yunohost.domain import _get_maindomain, _build_dns_conf
-from yunohost.utils.network import get_public_ip, dig
+from yunohost.utils.error import YunohostError, YunohostValidationError
+from yunohost.domain import _get_maindomain
+from yunohost.utils.network import get_public_ip
+from yunohost.utils.dns import dig
from yunohost.log import is_unit_operation
+from yunohost.regenconf import regen_conf
logger = getActionLogger("yunohost.dyndns")
@@ -121,10 +123,9 @@ def dyndns_subscribe(
subscribe_host -- Dynette HTTP API to subscribe to
"""
- if len(glob.glob("/etc/yunohost/dyndns/*.key")) != 0 or os.path.exists(
- "/etc/cron.d/yunohost-dyndns"
- ):
- raise YunohostError("domain_dyndns_already_subscribed")
+
+ if _guess_current_dyndns_domain(subscribe_host) != (None, None):
+ raise YunohostValidationError("domain_dyndns_already_subscribed")
if domain is None:
domain = _get_maindomain()
@@ -132,13 +133,13 @@ def dyndns_subscribe(
# Verify if domain is provided by subscribe_host
if not _dyndns_provides(subscribe_host, domain):
- raise YunohostError(
+ raise YunohostValidationError(
"dyndns_domain_not_provided", domain=domain, provider=subscribe_host
)
# Verify if domain is available
if not _dyndns_available(subscribe_host, domain):
- raise YunohostError("dyndns_unavailable", domain=domain)
+ raise YunohostValidationError("dyndns_unavailable", domain=domain)
operation_logger.start()
@@ -151,13 +152,12 @@ def dyndns_subscribe(
os.system(
"cd /etc/yunohost/dyndns && "
- "dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s"
- % domain
- )
- os.system(
- "chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private"
+ f"dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER {domain}"
)
+ chmod("/etc/yunohost/dyndns", 0o600, recursive=True)
+ chown("/etc/yunohost/dyndns", "root", recursive=True)
+
private_file = glob.glob("/etc/yunohost/dyndns/*%s*.private" % domain)[0]
key_file = glob.glob("/etc/yunohost/dyndns/*%s*.key" % domain)[0]
with open(key_file) as f:
@@ -169,26 +169,37 @@ def dyndns_subscribe(
try:
r = requests.post(
"https://%s/key/%s?key_algo=hmac-sha512"
- % (subscribe_host, base64.b64encode(key)),
+ % (subscribe_host, base64.b64encode(key.encode()).decode()),
data={"subdomain": domain},
timeout=30,
)
except Exception as e:
- os.system("rm -f %s" % private_file)
- os.system("rm -f %s" % key_file)
+ rm(private_file, force=True)
+ rm(key_file, force=True)
raise YunohostError("dyndns_registration_failed", error=str(e))
if r.status_code != 201:
- os.system("rm -f %s" % private_file)
- os.system("rm -f %s" % key_file)
+ rm(private_file, force=True)
+ rm(key_file, force=True)
try:
error = json.loads(r.text)["error"]
except Exception:
error = 'Server error, code: %s. (Message: "%s")' % (r.status_code, r.text)
raise YunohostError("dyndns_registration_failed", error=error)
- logger.success(m18n.n("dyndns_registered"))
+ # Yunohost regen conf will add the dyndns cron job if a private key exists
+ # in /etc/yunohost/dyndns
+ regen_conf(["yunohost"])
- dyndns_installcron()
+ # Add some dyndns update in 2 and 4 minutes from now such that user should
+ # not have to wait 10ish minutes for the conf to propagate
+ cmd = (
+ "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\""
+ )
+ # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity...
+ subprocess.check_call(["bash", "-c", cmd.format(t="2 min")])
+ subprocess.check_call(["bash", "-c", cmd.format(t="4 min")])
+
+ logger.success(m18n.n("dyndns_registered"))
@is_unit_operation()
@@ -213,20 +224,23 @@ def dyndns_update(
ipv6 -- IPv6 address to send
"""
- # Get old ipv4/v6
- old_ipv4, old_ipv6 = (None, None) # (default values)
+ from yunohost.dns import _build_dns_conf
# If domain is not given, try to guess it from keys available...
if domain is None:
(domain, key) = _guess_current_dyndns_domain(dyn_host)
+
+ if domain is None:
+ raise YunohostValidationError("dyndns_no_domain_registered")
+
# If key is not given, pick the first file we find with the domain given
else:
if key is None:
keys = glob.glob("/etc/yunohost/dyndns/K{0}.+*.private".format(domain))
if not keys:
- raise YunohostError("dyndns_key_not_found")
+ raise YunohostValidationError("dyndns_key_not_found")
key = keys[0]
@@ -247,7 +261,7 @@ def dyndns_update(
ok, result = dig(dyn_host, "A")
dyn_host_ip = result[0] if ok == "ok" and len(result) else None
if not dyn_host_ip:
- raise YunohostError("Failed to resolve %s" % dyn_host)
+ raise YunohostError("Failed to resolve %s" % dyn_host, raw_msg=True)
ok, result = dig(domain, rdtype, resolvers=[dyn_host_ip])
if ok == "ok":
@@ -292,6 +306,12 @@ def dyndns_update(
logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6))
logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6))
+ if ipv4 is None and ipv6 is None:
+ logger.debug(
+ "No ipv4 nor ipv6 ?! Sounds like the server is not connected to the internet, or the ip.yunohost.org infrastructure is down somehow"
+ )
+ return
+
# no need to update
if (not force and not dry_run) and (old_ipv4 == ipv4 and old_ipv6 == ipv6):
logger.info("No updated needed.")
@@ -361,29 +381,15 @@ def dyndns_update(
def dyndns_installcron():
- """
- Install IP update cron
-
-
- """
- with open("/etc/cron.d/yunohost-dyndns", "w+") as f:
- f.write("*/2 * * * * root yunohost dyndns update >> /dev/null\n")
-
- logger.success(m18n.n("dyndns_cron_installed"))
+ logger.warning(
+ "This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'."
+ )
def dyndns_removecron():
- """
- Remove IP update cron
-
-
- """
- try:
- os.remove("/etc/cron.d/yunohost-dyndns")
- except Exception as e:
- raise YunohostError("dyndns_cron_remove_failed", error=e)
-
- logger.success(m18n.n("dyndns_cron_removed"))
+ logger.warning(
+ "This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'."
+ )
def _guess_current_dyndns_domain(dyn_host):
@@ -414,4 +420,4 @@ def _guess_current_dyndns_domain(dyn_host):
else:
return (_domain, path)
- raise YunohostError("dyndns_no_domain_registered")
+ return (None, None)
diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py
index 1b708a626..a1c0b187f 100644
--- a/src/yunohost/firewall.py
+++ b/src/yunohost/firewall.py
@@ -28,10 +28,9 @@ import yaml
import miniupnpc
from moulinette import m18n
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils import process
from moulinette.utils.log import getActionLogger
-from moulinette.utils.text import prependlines
FIREWALL_FILE = "/etc/yunohost/firewall.yml"
UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp"
@@ -179,7 +178,7 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False):
"""
with open(FIREWALL_FILE) as f:
- firewall = yaml.load(f)
+ firewall = yaml.safe_load(f)
if raw:
return firewall
@@ -188,18 +187,25 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False):
for i in ["ipv4", "ipv6"]:
f = firewall[i]
# Combine TCP and UDP ports
- ports[i] = sorted(set(f["TCP"]) | set(f["UDP"]))
+ ports[i] = sorted(
+ set(f["TCP"]) | set(f["UDP"]),
+ key=lambda p: int(p.split(":")[0]) if isinstance(p, str) else p,
+ )
if not by_ip_version:
# Combine IPv4 and IPv6 ports
- ports = sorted(set(ports["ipv4"]) | set(ports["ipv6"]))
+ ports = sorted(
+ set(ports["ipv4"]) | set(ports["ipv6"]),
+ key=lambda p: int(p.split(":")[0]) if isinstance(p, str) else p,
+ )
# Format returned dict
ret = {"opened_ports": ports}
if list_forwarded:
# Combine TCP and UDP forwarded ports
ret["forwarded_ports"] = sorted(
- set(firewall["uPnP"]["TCP"]) | set(firewall["uPnP"]["UDP"])
+ set(firewall["uPnP"]["TCP"]) | set(firewall["uPnP"]["UDP"]),
+ key=lambda p: int(p.split(":")[0]) if isinstance(p, str) else p,
)
return ret
@@ -233,7 +239,7 @@ def firewall_reload(skip_upnp=False):
except process.CalledProcessError as e:
logger.debug(
"iptables seems to be not available, it outputs:\n%s",
- prependlines(e.output.rstrip(), "> "),
+ e.output.decode().strip(),
)
logger.warning(m18n.n("iptables_unavailable"))
else:
@@ -266,7 +272,7 @@ def firewall_reload(skip_upnp=False):
except process.CalledProcessError as e:
logger.debug(
"ip6tables seems to be not available, it outputs:\n%s",
- prependlines(e.output.rstrip(), "> "),
+ e.output.decode().strip(),
)
logger.warning(m18n.n("ip6tables_unavailable"))
else:
@@ -366,7 +372,7 @@ def firewall_upnp(action="status", no_refresh=False):
if action == "status":
no_refresh = True
else:
- raise YunohostError("action_invalid", action=action)
+ raise YunohostValidationError("action_invalid", action=action)
# Refresh port mapping using UPnP
if not no_refresh:
@@ -392,6 +398,12 @@ def firewall_upnp(action="status", no_refresh=False):
for protocol in ["TCP", "UDP"]:
if protocol + "_TO_CLOSE" in firewall["uPnP"]:
for port in firewall["uPnP"][protocol + "_TO_CLOSE"]:
+
+ if not isinstance(port, int):
+ # FIXME : how should we handle port ranges ?
+ logger.warning("Can't use UPnP to close '%s'" % port)
+ continue
+
# Clean the mapping of this port
if upnpc.getspecificportmapping(port, protocol):
try:
@@ -401,6 +413,12 @@ def firewall_upnp(action="status", no_refresh=False):
firewall["uPnP"][protocol + "_TO_CLOSE"] = []
for port in firewall["uPnP"][protocol]:
+
+ if not isinstance(port, int):
+ # FIXME : how should we handle port ranges ?
+ logger.warning("Can't use UPnP to open '%s'" % port)
+ continue
+
# Clean the mapping of this port
if upnpc.getspecificportmapping(port, protocol):
try:
@@ -507,6 +525,6 @@ def _on_rule_command_error(returncode, cmd, output):
'"%s" returned non-zero exit status %d:\n%s',
cmd,
returncode,
- prependlines(output.rstrip(), "> "),
+ output.decode().strip(),
)
return True
diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py
index e9857e4f9..20757bf3c 100644
--- a/src/yunohost/hook.py
+++ b/src/yunohost/hook.py
@@ -31,10 +31,10 @@ import mimetypes
from glob import iglob
from importlib import import_module
-from moulinette import m18n, msettings
-from yunohost.utils.error import YunohostError
+from moulinette import m18n, Moulinette
+from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils import log
-from moulinette.utils.filesystem import read_json
+from moulinette.utils.filesystem import read_yaml, cp
HOOK_FOLDER = "/usr/share/yunohost/hooks/"
CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/"
@@ -60,8 +60,7 @@ def hook_add(app, file):
os.makedirs(CUSTOM_HOOK_FOLDER + action)
finalpath = CUSTOM_HOOK_FOLDER + action + "/" + priority + "-" + app
- os.system("cp %s %s" % (file, finalpath))
- os.system("chown -hR admin: %s" % HOOK_FOLDER)
+ cp(file, finalpath)
return {"hook": finalpath}
@@ -117,7 +116,7 @@ def hook_info(action, name):
)
if not hooks:
- raise YunohostError("hook_name_unknown", name=name)
+ raise YunohostValidationError("hook_name_unknown", name=name)
return {
"action": action,
"name": name,
@@ -186,7 +185,7 @@ def hook_list(action, list_by="name", show_info=False):
d.add(name)
else:
- raise YunohostError("hook_list_by_invalid")
+ raise YunohostValidationError("hook_list_by_invalid")
def _append_folder(d, folder):
# Iterate over and add hook from a folder
@@ -273,7 +272,7 @@ def hook_callback(
try:
hl = hooks_names[n]
except KeyError:
- raise YunohostError("hook_name_unknown", n)
+ raise YunohostValidationError("hook_name_unknown", n)
# Iterate over hooks with this name
for h in hl:
# Update hooks dict
@@ -320,7 +319,13 @@ def hook_callback(
def hook_exec(
- path, args=None, raise_on_error=False, chdir=None, env=None, return_format="json"
+ path,
+ args=None,
+ raise_on_error=False,
+ chdir=None,
+ env=None,
+ user="root",
+ return_format="yaml",
):
"""
Execute hook from a file with arguments
@@ -331,6 +336,7 @@ def hook_exec(
raise_on_error -- Raise if the script returns a non-zero exit code
chdir -- The directory from where the script will be executed
env -- Dictionnary of environment variables to export
+ user -- User with which to run the command
"""
# Validate hook path
@@ -372,7 +378,7 @@ def hook_exec(
returncode, returndata = _hook_exec_python(path, args, env, loggers)
else:
returncode, returndata = _hook_exec_bash(
- path, args, chdir, env, return_format, loggers
+ path, args, chdir, env, user, return_format, loggers
)
# Check and return process' return code
@@ -388,7 +394,7 @@ def hook_exec(
return returncode, returndata
-def _hook_exec_bash(path, args, chdir, env, return_format, loggers):
+def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers):
from moulinette.utils.process import call_async_output
@@ -409,24 +415,30 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers):
env = {}
env["YNH_CWD"] = chdir
- env["YNH_INTERFACE"] = msettings.get("interface")
+ env["YNH_INTERFACE"] = Moulinette.interface.type
stdreturn = os.path.join(tempfile.mkdtemp(), "stdreturn")
with open(stdreturn, "w") as f:
f.write("")
env["YNH_STDRETURN"] = stdreturn
+ # Construct command to execute
+ if user == "root":
+ command = ["sh", "-c"]
+ else:
+ command = ["sudo", "-n", "-u", user, "-H", "sh", "-c"]
+
# use xtrace on fd 7 which is redirected to stdout
env["BASH_XTRACEFD"] = "7"
cmd = '/bin/bash -x "{script}" {args} 7>&1'
- cmd = cmd.format(script=cmd_script, args=cmd_args)
+ command.append(cmd.format(script=cmd_script, args=cmd_args))
- logger.debug("Executing command '%s'" % cmd)
+ logger.debug("Executing command '%s'" % command)
_env = os.environ.copy()
_env.update(env)
- returncode = call_async_output(cmd, loggers, shell=True, cwd=chdir, env=_env)
+ returncode = call_async_output(command, loggers, shell=False, cwd=chdir, env=_env)
raw_content = None
try:
@@ -434,10 +446,10 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers):
raw_content = f.read()
returncontent = {}
- if return_format == "json":
+ if return_format == "yaml":
if raw_content != "":
try:
- returncontent = read_json(stdreturn)
+ returncontent = read_yaml(stdreturn)
except Exception as e:
raise YunohostError(
"hook_json_return_error",
@@ -485,6 +497,40 @@ def _hook_exec_python(path, args, env, loggers):
return ret
+def hook_exec_with_script_debug_if_failure(*args, **kwargs):
+
+ operation_logger = kwargs.pop("operation_logger")
+ error_message_if_failed = kwargs.pop("error_message_if_failed")
+ error_message_if_script_failed = kwargs.pop("error_message_if_script_failed")
+
+ failed = True
+ failure_message_with_debug_instructions = None
+ try:
+ retcode, retpayload = hook_exec(*args, **kwargs)
+ failed = True if retcode != 0 else False
+ if failed:
+ error = error_message_if_script_failed
+ logger.error(error_message_if_failed(error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
+ if Moulinette.interface.type != "api":
+ operation_logger.dump_script_log_extract_for_debugging()
+ # Script got manually interrupted ...
+ # N.B. : KeyboardInterrupt does not inherit from Exception
+ except (KeyboardInterrupt, EOFError):
+ error = m18n.n("operation_interrupted")
+ logger.error(error_message_if_failed(error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
+ # Something wrong happened in Yunohost's code (most probably hook_exec)
+ except Exception:
+ import traceback
+
+ error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
+ logger.error(error_message_if_failed(error))
+ failure_message_with_debug_instructions = operation_logger.error(error)
+
+ return failed, failure_message_with_debug_instructions
+
+
def _extract_filename_parts(filename):
"""Extract hook parts from filename"""
if "-" in filename:
diff --git a/src/yunohost/log.py b/src/yunohost/log.py
index 24ecc6713..d73a62cd0 100644
--- a/src/yunohost/log.py
+++ b/src/yunohost/log.py
@@ -29,13 +29,15 @@ import re
import yaml
import glob
import psutil
+from typing import List
from datetime import datetime, timedelta
from logging import FileHandler, getLogger, Formatter
+from io import IOBase
-from moulinette import m18n, msettings
+from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.packages import get_ynh_package_version
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, read_yaml
@@ -44,7 +46,6 @@ CATEGORIES_PATH = "/var/log/yunohost/categories/"
OPERATIONS_PATH = "/var/log/yunohost/categories/operation/"
METADATA_FILE_EXT = ".yml"
LOG_FILE_EXT = ".log"
-RELATED_CATEGORIES = ["app", "domain", "group", "service", "user"]
logger = getActionLogger("yunohost.log")
@@ -69,7 +70,13 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
logs = list(reversed(sorted(logs)))
if limit is not None:
- logs = logs[:limit]
+ if with_suboperations:
+ logs = logs[:limit]
+ else:
+ # If we displaying only parent, we are still gonna load up to limit * 5 logs
+ # because many of them are suboperations which are not gonna be kept
+ # Yet we still want to obtain ~limit number of logs
+ logs = logs[: limit * 5]
for log in logs:
@@ -122,10 +129,13 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
else:
operations = [o for o in operations.values()]
+ if limit:
+ operations = operations[:limit]
+
operations = list(reversed(sorted(operations, key=lambda o: o["name"])))
# Reverse the order of log when in cli, more comfortable to read (avoid
# unecessary scrolling)
- is_api = msettings.get("interface") == "api"
+ is_api = Moulinette.interface.type == "api"
if not is_api:
operations = list(reversed(operations))
@@ -151,26 +161,42 @@ def log_show(
filter_irrelevant = True
if filter_irrelevant:
- filters = [
- r"set [+-]x$",
- r"set [+-]o xtrace$",
- r"local \w+$",
- r"local legacy_args=.*$",
- r".*Helper used in legacy mode.*",
- r"args_array=.*$",
- r"local -A args_array$",
- r"ynh_handle_getopts_args",
- r"ynh_script_progression",
- ]
+
+ def _filter(lines):
+ filters = [
+ r"set [+-]x$",
+ r"set [+-]o xtrace$",
+ r"set [+-]o errexit$",
+ r"set [+-]o nounset$",
+ r"trap '' EXIT",
+ r"local \w+$",
+ r"local exit_code=(1|0)$",
+ r"local legacy_args=.*$",
+ r"local -A args_array$",
+ r"args_array=.*$",
+ r"ret_code=1",
+ r".*Helper used in legacy mode.*",
+ r"ynh_handle_getopts_args",
+ r"ynh_script_progression",
+ r"sleep 0.5",
+ r"'\[' (1|0) -eq (1|0) '\]'$",
+ r"\[?\['? -n '' '?\]\]?$",
+ r"rm -rf /var/cache/yunohost/download/$",
+ r"type -t ynh_clean_setup$",
+ r"DEBUG - \+ echo '",
+ r"DEBUG - \+ exit (1|0)$",
+ ]
+ filters = [re.compile(f) for f in filters]
+ return [
+ line
+ for line in lines
+ if not any(f.search(line.strip()) for f in filters)
+ ]
+
else:
- filters = []
- def _filter_lines(lines, filters=[]):
-
- filters = [re.compile(f) for f in filters]
- return [
- line for line in lines if not any(f.search(line.strip()) for f in filters)
- ]
+ def _filter(lines):
+ return lines
# Normalize log/metadata paths and filenames
abs_path = path
@@ -191,7 +217,7 @@ def log_show(
log_path = base_path + LOG_FILE_EXT
if not os.path.exists(md_path) and not os.path.exists(log_path):
- raise YunohostError("log_does_exists", log=path)
+ raise YunohostValidationError("log_does_exists", log=path)
infos = {}
@@ -209,12 +235,12 @@ def log_show(
content += "\n============\n\n"
if os.path.exists(log_path):
actual_log = read_file(log_path)
- content += "\n".join(_filter_lines(actual_log.split("\n"), filters))
+ content += "\n".join(_filter(actual_log.split("\n")))
url = yunopaste(content)
logger.info(m18n.n("log_available_on_yunopaste", url=url))
- if msettings.get("interface") == "api":
+ if Moulinette.interface.type == "api":
return {"url": url}
else:
return
@@ -222,7 +248,7 @@ def log_show(
# Display metadata if exist
if os.path.exists(md_path):
try:
- metadata = read_yaml(md_path)
+ metadata = read_yaml(md_path) or {}
except MoulinetteError as e:
error = m18n.n("log_corrupted_md_file", md_file=md_path, error=e)
if os.path.exists(log_path):
@@ -282,13 +308,13 @@ def log_show(
if os.path.exists(log_path):
from yunohost.service import _tail
- if number and filters:
+ if number and filter_irrelevant:
logs = _tail(log_path, int(number * 4))
elif number:
logs = _tail(log_path, int(number))
else:
logs = read_file(log_path)
- logs = _filter_lines(logs, filters)
+ logs = list(_filter(logs))
if number:
logs = logs[-number:]
infos["log_path"] = log_path
@@ -340,9 +366,9 @@ def is_unit_operation(
# Indeed, we use convention naming in this decorator and we need to
# know name of each args (so we need to use kwargs instead of args)
if len(args) > 0:
- from inspect import getargspec
+ from inspect import signature
- keys = getargspec(func).args
+ keys = list(signature(func).parameters.keys())
if "operation_logger" in keys:
keys.remove("operation_logger")
for k, arg in enumerate(args):
@@ -371,6 +397,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 Exception:
+ context[field] = "IOBase"
operation_logger = OperationLogger(op_key, related_to, args=context)
try:
@@ -399,7 +437,11 @@ class RedactingFormatter(Formatter):
msg = super(RedactingFormatter, self).format(record)
self.identify_data_to_redact(msg)
for data in self.data_to_redact:
- msg = msg.replace(data, "**********")
+ # we check that data is not empty string,
+ # otherwise this may lead to super epic stuff
+ # (try to run "foo".replace("", "bar"))
+ if data:
+ msg = msg.replace(data, "**********")
return msg
def identify_data_to_redact(self, record):
@@ -411,7 +453,8 @@ class RedactingFormatter(Formatter):
# (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=")
# Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest
match = re.search(
- r"(pwd|pass|password|secret|\w+key|token)=(\S{3,})$", record.strip()
+ r"(pwd|pass|passwd|password|passphrase|secret\w*|\w+key|token|PASSPHRASE)=(\S{3,})$",
+ record.strip(),
)
if (
match
@@ -436,7 +479,7 @@ class OperationLogger(object):
This class record logs and metadata like context or start time/end time.
"""
- _instances = []
+ _instances: List[object] = []
def __init__(self, operation, related_to=None, **kwargs):
# TODO add a way to not save password on app installation
@@ -604,7 +647,7 @@ class OperationLogger(object):
"operation": self.operation,
"parent": self.parent,
"yunohost_version": get_ynh_package_version("yunohost")["version"],
- "interface": msettings.get("interface"),
+ "interface": Moulinette.interface.type,
}
if self.related_to is not None:
data["related_to"] = self.related_to
@@ -633,10 +676,23 @@ class OperationLogger(object):
"""
Close properly the unit operation
"""
+
+ # When the error happen's in the is_unit_operation try/except,
+ # we want to inject the log ref in the exception, such that it may be
+ # transmitted to the webadmin which can then redirect to the appropriate
+ # log page
+ if (
+ self.started_at
+ and isinstance(error, Exception)
+ and not isinstance(error, YunohostValidationError)
+ ):
+ error.log_ref = self.name
+
if self.ended_at is not None or self.started_at is None:
return
if error is not None and not isinstance(error, str):
error = str(error)
+
self.ended_at = datetime.utcnow()
self._error = error
self._success = error is None
@@ -645,7 +701,7 @@ class OperationLogger(object):
self.logger.removeHandler(self.file_handler)
self.file_handler.close()
- is_api = msettings.get("interface") == "api"
+ is_api = Moulinette.interface.type == "api"
desc = _get_description_from_name(self.name)
if error is None:
if is_api:
@@ -677,6 +733,52 @@ class OperationLogger(object):
else:
self.error(m18n.n("log_operation_unit_unclosed_properly"))
+ def dump_script_log_extract_for_debugging(self):
+
+ with open(self.log_path, "r") as f:
+ lines = f.readlines()
+
+ filters = [
+ r"set [+-]x$",
+ r"set [+-]o xtrace$",
+ r"local \w+$",
+ r"local legacy_args=.*$",
+ r".*Helper used in legacy mode.*",
+ r"args_array=.*$",
+ r"local -A args_array$",
+ r"ynh_handle_getopts_args",
+ r"ynh_script_progression",
+ ]
+
+ filters = [re.compile(f_) for f_ in filters]
+
+ lines_to_display = []
+ for line in lines:
+
+ if ": " not in line.strip():
+ continue
+
+ # A line typically looks like
+ # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo
+ # And we just want the part starting by "DEBUG - "
+ line = line.strip().split(": ", 1)[1]
+
+ if any(filter_.search(line) for filter_ in filters):
+ continue
+
+ lines_to_display.append(line)
+
+ if line.endswith("+ ynh_exit_properly") or " + ynh_die " in line:
+ break
+ elif len(lines_to_display) > 20:
+ lines_to_display.pop(0)
+
+ logger.warning(
+ "Here's an extract of the logs before the crash. It might help debugging the error:"
+ )
+ for line in lines_to_display:
+ logger.info(line)
+
def _get_datetime_from_name(name):
diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py
index 3cd67b148..1856046d6 100644
--- a/src/yunohost/permission.py
+++ b/src/yunohost/permission.py
@@ -31,7 +31,7 @@ import random
from moulinette import m18n
from moulinette.utils.log import getActionLogger
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.log import is_unit_operation
logger = getActionLogger("yunohost.user")
@@ -46,7 +46,7 @@ SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"]
def user_permission_list(
- short=False, full=False, ignore_system_perms=False, absolute_urls=False
+ short=False, full=False, ignore_system_perms=False, absolute_urls=False, apps=[]
):
"""
List permissions and corresponding accesses
@@ -74,22 +74,28 @@ def user_permission_list(
)
# Parse / organize information to be outputed
- apps = sorted(_installed_apps())
+ installed_apps = sorted(_installed_apps())
+ filter_ = apps
+ apps = filter_ if filter_ else installed_apps
apps_base_path = {
app: app_setting(app, "domain") + app_setting(app, "path")
for app in apps
- if app_setting(app, "domain") and app_setting(app, "path")
+ if app in installed_apps
+ and app_setting(app, "domain")
+ and app_setting(app, "path")
}
permissions = {}
for infos in permissions_infos:
name = infos["cn"][0]
- if ignore_system_perms and name.split(".")[0] in SYSTEM_PERMS:
- continue
-
app = name.split(".")[0]
+ if ignore_system_perms and app in SYSTEM_PERMS:
+ continue
+ if filter_ and app not in apps:
+ continue
+
perm = {}
perm["allowed"] = [
_ldap_path_extract(p, "cn") for p in infos.get("groupPermission", [])
@@ -175,14 +181,26 @@ def user_permission_update(
# Refuse to add "visitors" to mail, xmpp ... they require an account to make sense.
if add and "visitors" in add and permission.split(".")[0] in SYSTEM_PERMS:
- raise YunohostError("permission_require_account", permission=permission)
+ raise YunohostValidationError(
+ "permission_require_account", permission=permission
+ )
# Refuse to add "visitors" to protected permission
if (
(add and "visitors" in add and existing_permission["protected"])
or (remove and "visitors" in remove and existing_permission["protected"])
) and not force:
- raise YunohostError("permission_protected", permission=permission)
+ raise YunohostValidationError("permission_protected", permission=permission)
+
+ # Refuse to add "all_users" to ssh/sftp permissions
+ if (
+ permission.split(".")[0] in ["ssh", "sftp"]
+ and (add and "all_users" in add)
+ and not force
+ ):
+ raise YunohostValidationError(
+ "permission_cant_add_to_all_users", permission=permission
+ )
# Fetch currently allowed groups for this permission
@@ -198,7 +216,7 @@ def user_permission_update(
groups_to_add = [add] if not isinstance(add, list) else add
for group in groups_to_add:
if group not in all_existing_groups:
- raise YunohostError("group_unknown", group=group)
+ raise YunohostValidationError("group_unknown", group=group)
if group in current_allowed_groups:
logger.warning(
m18n.n(
@@ -326,7 +344,7 @@ def user_permission_info(permission):
permission, None
)
if existing_permission is None:
- raise YunohostError("permission_not_found", permission=permission)
+ raise YunohostValidationError("permission_not_found", permission=permission)
return existing_permission
@@ -391,7 +409,7 @@ def permission_create(
if ldap.get_conflict(
{"cn": permission}, base_dn="ou=permission,dc=yunohost,dc=org"
):
- raise YunohostError("permission_already_exist", permission=permission)
+ raise YunohostValidationError("permission_already_exist", permission=permission)
# Get random GID
all_gid = {x.gr_gid for x in grp.getgrall()}
@@ -427,7 +445,7 @@ def permission_create(
all_existing_groups = user_group_list()["groups"].keys()
for group in allowed or []:
if group not in all_existing_groups:
- raise YunohostError("group_unknown", group=group)
+ raise YunohostValidationError("group_unknown", group=group)
operation_logger.related_to.append(("app", permission.split(".")[0]))
operation_logger.start()
@@ -439,22 +457,26 @@ def permission_create(
"permission_creation_failed", permission=permission, error=e
)
- permission_url(
- permission,
- url=url,
- add_url=additional_urls,
- auth_header=auth_header,
- sync_perm=False,
- )
+ try:
+ permission_url(
+ permission,
+ url=url,
+ add_url=additional_urls,
+ auth_header=auth_header,
+ sync_perm=False,
+ )
- new_permission = _update_ldap_group_permission(
- permission=permission,
- allowed=allowed,
- label=label,
- show_tile=show_tile,
- protected=protected,
- sync_perm=sync_perm,
- )
+ new_permission = _update_ldap_group_permission(
+ permission=permission,
+ allowed=allowed,
+ label=label,
+ show_tile=show_tile,
+ protected=protected,
+ sync_perm=sync_perm,
+ )
+ except Exception:
+ permission_delete(permission, force=True)
+ raise
logger.debug(m18n.n("permission_created", permission=permission))
return new_permission
@@ -594,7 +616,7 @@ def permission_delete(operation_logger, permission, force=False, sync_perm=True)
permission = permission + ".main"
if permission.endswith(".main") and not force:
- raise YunohostError("permission_cannot_remove_main")
+ raise YunohostValidationError("permission_cannot_remove_main")
from yunohost.utils.ldap import _get_ldap_interface
@@ -842,11 +864,9 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
re:^/api/.*|/scripts/api.js$
"""
- from yunohost.domain import domain_list
+ from yunohost.domain import _assert_domain_exists
from yunohost.app import _assert_no_conflicting_apps
- domains = domain_list()["domains"]
-
#
# Regexes
#
@@ -861,7 +881,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
try:
re.compile(regex)
except Exception:
- raise YunohostError("invalid_regex", regex=regex)
+ raise YunohostValidationError("invalid_regex", regex=regex)
if url.startswith("re:"):
@@ -874,12 +894,12 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
# regex with domain
if "/" not in url:
- raise YunohostError("regex_with_only_domain")
+ raise YunohostValidationError("regex_with_only_domain")
domain, path = url[3:].split("/", 1)
path = "/" + path
- if domain.replace("%", "").replace("\\", "") not in domains:
- raise YunohostError("domain_name_unknown", domain=domain)
+ domain_with_no_regex = domain.replace("%", "").replace("\\", "")
+ _assert_domain_exists(domain_with_no_regex)
validate_regex(path)
@@ -913,8 +933,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
domain, path = split_domain_path(url)
sanitized_url = domain + path
- if domain not in domains:
- raise YunohostError("domain_name_unknown", domain=domain)
+ _assert_domain_exists(domain)
_assert_no_conflicting_apps(domain, path, ignore_app=app)
diff --git a/src/yunohost/regenconf.py b/src/yunohost/regenconf.py
index 924818e44..1beef8a44 100644
--- a/src/yunohost/regenconf.py
+++ b/src/yunohost/regenconf.py
@@ -105,13 +105,9 @@ def regen_conf(
else:
filesystem.mkdir(PENDING_CONF_DIR, 0o755, True)
- # Format common hooks arguments
- common_args = [1 if force else 0, 1 if dry_run else 0]
-
# Execute hooks for pre-regen
- pre_args = [
- "pre",
- ] + common_args
+ # element 2 and 3 with empty string is because of legacy...
+ pre_args = ["pre", "", ""]
def _pre_call(name, priority, path, args):
# create the pending conf directory for the category
@@ -139,6 +135,9 @@ def regen_conf(
if "glances" in names:
names.remove("glances")
+ if "avahi-daemon" in names:
+ names.remove("avahi-daemon")
+
# [Optimization] We compute and feed the domain list to the conf regen
# hooks to avoid having to call "yunohost domain list" so many times which
# ends up in wasted time (about 3~5 seconds per call on a RPi2)
@@ -417,9 +416,8 @@ def regen_conf(
return result
# Execute hooks for post-regen
- post_args = [
- "post",
- ] + common_args
+ # element 2 and 3 with empty string is because of legacy...
+ post_args = ["post", "", ""]
def _pre_call(name, priority, path, args):
# append coma-separated applied changes for the category
@@ -444,7 +442,7 @@ def _get_regenconf_infos():
"""
try:
with open(REGEN_CONF_FILE, "r") as f:
- return yaml.load(f)
+ return yaml.safe_load(f)
except Exception:
return {}
@@ -460,6 +458,10 @@ def _save_regenconf_infos(infos):
if "glances" in infos:
del infos["glances"]
+ # Ugly hack to get rid of legacy avahi stuff
+ if "avahi-daemon" in infos:
+ del infos["avahi-daemon"]
+
try:
with open(REGEN_CONF_FILE, "w") as f:
yaml.safe_dump(infos, f, default_flow_style=False)
diff --git a/src/yunohost/service.py b/src/yunohost/service.py
index bc72301da..f200d08c0 100644
--- a/src/yunohost/service.py
+++ b/src/yunohost/service.py
@@ -34,13 +34,22 @@ from glob import glob
from datetime import datetime
from moulinette import m18n
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.process import check_output
from moulinette.utils.log import getActionLogger
-from moulinette.utils.filesystem import read_file, append_to_file, write_to_file
+from moulinette.utils.filesystem import (
+ read_file,
+ append_to_file,
+ write_to_file,
+ read_yaml,
+ write_to_yaml,
+)
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"
+SERVICES_CONF = "/etc/yunohost/services.yml"
+SERVICES_CONF_BASE = "/usr/share/yunohost/templates/yunohost/services.yml"
+
logger = getActionLogger("yunohost.service")
@@ -127,7 +136,8 @@ def service_add(
try:
_save_services(services)
- except Exception:
+ except Exception as e:
+ logger.warning(e)
# we'll get a logger.warning with more details in _save_services
raise YunohostError("service_add_failed", service=name)
@@ -145,7 +155,7 @@ def service_remove(name):
services = _get_services()
if name not in services:
- raise YunohostError("service_unknown", service=name)
+ raise YunohostValidationError("service_unknown", service=name)
del services[name]
try:
@@ -246,7 +256,7 @@ def service_restart(names):
)
-def service_reload_or_restart(names):
+def service_reload_or_restart(names, test_conf=True):
"""
Reload one or more services if they support it. If not, restart them instead. If the services are not running yet, they will be started.
@@ -256,7 +266,36 @@ def service_reload_or_restart(names):
"""
if isinstance(names, str):
names = [names]
+
+ services = _get_services()
+
for name in names:
+
+ logger.debug(f"Reloading service {name}")
+
+ test_conf_cmd = services.get(name, {}).get("test_conf")
+ if test_conf and test_conf_cmd:
+
+ p = subprocess.Popen(
+ test_conf_cmd,
+ shell=True,
+ executable="/bin/bash",
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+
+ out, _ = p.communicate()
+ if p.returncode != 0:
+ errors = out.decode().strip().split("\n")
+ logger.error(
+ m18n.n(
+ "service_not_reloading_because_conf_broken",
+ name=name,
+ errors=errors,
+ )
+ )
+ continue
+
if _run_service_command("reload-or-restart", name):
logger.success(m18n.n("service_reloaded_or_restarted", service=name))
else:
@@ -325,7 +364,7 @@ def service_status(names=[]):
# Validate service names requested
for name in names:
if name not in services.keys():
- raise YunohostError("service_unknown", service=name)
+ raise YunohostValidationError("service_unknown", service=name)
# Filter only requested servivces
services = {k: v for k, v in services.items() if k in names}
@@ -397,14 +436,11 @@ def _get_and_format_service_status(service, infos):
# If no description was there, try to get it from the .json locales
if not description:
- translation_key = "service_description_%s" % service
- description = m18n.n(translation_key)
- # If descrption is still equal to the translation key,
- # that mean that we don't have a translation for this string
- # that's the only way to test for that for now
- # if we don't have it, uses the one provided by systemd
- if description == translation_key:
+ translation_key = "service_description_%s" % service
+ if m18n.key_exists(translation_key):
+ description = m18n.n(translation_key)
+ else:
description = str(raw_status.get("Description", ""))
output = {
@@ -468,6 +504,7 @@ def _get_and_format_service_status(service, infos):
if p.returncode == 0:
output["configuration"] = "valid"
else:
+ out = out.decode()
output["configuration"] = "broken"
output["configuration-details"] = out.strip().split("\n")
@@ -487,7 +524,7 @@ def service_log(name, number=50):
number = int(number)
if name not in services.keys():
- raise YunohostError("service_unknown", service=name)
+ raise YunohostValidationError("service_unknown", service=name)
log_list = services[name].get("log", [])
@@ -548,7 +585,7 @@ def service_regen_conf(
for name in names:
if name not in services.keys():
- raise YunohostError("service_unknown", service=name)
+ raise YunohostValidationError("service_unknown", service=name)
if names is []:
names = list(services.keys())
@@ -571,7 +608,7 @@ def _run_service_command(action, service):
"""
services = _get_services()
if service not in services.keys():
- raise YunohostError("service_unknown", service=service)
+ raise YunohostValidationError("service_unknown", service=service)
possible_actions = [
"start",
@@ -671,17 +708,21 @@ def _get_services():
"""
try:
- with open("/etc/yunohost/services.yml", "r") as f:
- services = yaml.load(f) or {}
+ services = read_yaml(SERVICES_CONF_BASE) or {}
+
+ # These are keys flagged 'null' in the base conf
+ legacy_keys_to_delete = [k for k, v in services.items() if v is None]
+
+ services.update(read_yaml(SERVICES_CONF) or {})
+
+ services = {
+ name: infos
+ for name, infos in services.items()
+ if name not in legacy_keys_to_delete
+ }
except Exception:
return {}
- # some services are marked as None to remove them from YunoHost
- # filter this
- for key, value in list(services.items()):
- if value is None:
- del services[key]
-
# Dirty hack to automatically find custom SSH port ...
ssh_port_line = re.findall(
r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config")
@@ -705,6 +746,13 @@ def _get_services():
del services["postgresql"]["description"]
services["postgresql"]["actual_systemd_service"] = "postgresql@11-main"
+ # Remove legacy /var/log/daemon.log and /var/log/syslog from log entries
+ # because they are too general. Instead, now the journalctl log is
+ # returned by default which is more relevant.
+ for infos in services.values():
+ if infos.get("log") in ["/var/log/syslog", "/var/log/daemon.log"]:
+ del infos["log"]
+
return services
@@ -716,12 +764,26 @@ def _save_services(services):
services -- A dict of managed services with their parameters
"""
- try:
- with open("/etc/yunohost/services.yml", "w") as f:
- yaml.safe_dump(services, f, default_flow_style=False)
- except Exception as e:
- logger.warning("Error while saving services, exception: %s", e, exc_info=1)
- raise
+
+ # Compute the diff with the base file
+ # such that /etc/yunohost/services.yml contains the minimal
+ # changes with respect to the base conf
+
+ conf_base = yaml.safe_load(open(SERVICES_CONF_BASE)) or {}
+
+ diff = {}
+
+ for service_name, service_infos in services.items():
+ service_conf_base = conf_base.get(service_name, {})
+ diff[service_name] = {}
+
+ for key, value in service_infos.items():
+ if service_conf_base.get(key) != value:
+ diff[service_name][key] = value
+
+ diff = {name: infos for name, infos in diff.items() if infos}
+
+ write_to_yaml(SERVICES_CONF, diff)
def _tail(file, n):
diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py
index 9bf75ff1d..d59b41a58 100644
--- a/src/yunohost/settings.py
+++ b/src/yunohost/settings.py
@@ -6,9 +6,10 @@ from datetime import datetime
from collections import OrderedDict
from moulinette import m18n
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.log import getActionLogger
from yunohost.regenconf import regen_conf
+from yunohost.firewall import firewall_reload
logger = getActionLogger("yunohost.settings")
@@ -17,6 +18,9 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
def is_boolean(value):
+ TRUE = ["true", "on", "yes", "y", "1"]
+ FALSE = ["false", "off", "no", "n", "0"]
+
"""
Ensure a string value is intended as a boolean
@@ -29,9 +33,11 @@ def is_boolean(value):
"""
if isinstance(value, bool):
return True, value
+ if value in [0, 1]:
+ return True, bool(value)
elif isinstance(value, str):
- if str(value).lower() in ["true", "on", "yes", "false", "off", "no"]:
- return True, str(value).lower() in ["true", "on", "yes"]
+ if str(value).lower() in TRUE + FALSE:
+ return True, str(value).lower() in TRUE
else:
return False, None
else:
@@ -71,6 +77,17 @@ DEFAULTS = OrderedDict(
"choices": ["intermediate", "modern"],
},
),
+ (
+ "security.ssh.port",
+ {"type": "int", "default": 22},
+ ),
+ (
+ "security.nginx.redirect_to_https",
+ {
+ "type": "bool",
+ "default": True,
+ },
+ ),
(
"security.nginx.compatibility",
{
@@ -94,6 +111,10 @@ DEFAULTS = OrderedDict(
("smtp.relay.user", {"type": "string", "default": ""}),
("smtp.relay.password", {"type": "string", "default": ""}),
("backup.compress_tar_archives", {"type": "bool", "default": False}),
+ ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}),
+ ("security.webadmin.allowlist.enabled", {"type": "bool", "default": False}),
+ ("security.webadmin.allowlist", {"type": "string", "default": ""}),
+ ("security.experimental.enabled", {"type": "bool", "default": False}),
]
)
@@ -109,7 +130,9 @@ def settings_get(key, full=False):
settings = _get_settings()
if key not in settings:
- raise YunohostError("global_settings_key_doesnt_exists", settings_key=key)
+ raise YunohostValidationError(
+ "global_settings_key_doesnt_exists", settings_key=key
+ )
if full:
return settings[key]
@@ -137,7 +160,9 @@ def settings_set(key, value):
settings = _get_settings()
if key not in settings:
- raise YunohostError("global_settings_key_doesnt_exists", settings_key=key)
+ raise YunohostValidationError(
+ "global_settings_key_doesnt_exists", settings_key=key
+ )
key_type = settings[key]["type"]
@@ -146,7 +171,7 @@ def settings_set(key, value):
if boolean_value[0]:
value = boolean_value[1]
else:
- raise YunohostError(
+ raise YunohostValidationError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
@@ -158,14 +183,14 @@ def settings_set(key, value):
try:
value = int(value)
except Exception:
- raise YunohostError(
+ raise YunohostValidationError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
expected_type=key_type,
)
else:
- raise YunohostError(
+ raise YunohostValidationError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
@@ -173,7 +198,7 @@ def settings_set(key, value):
)
elif key_type == "string":
if not isinstance(value, str):
- raise YunohostError(
+ raise YunohostValidationError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
@@ -181,14 +206,14 @@ def settings_set(key, value):
)
elif key_type == "enum":
if value not in settings[key]["choices"]:
- raise YunohostError(
+ raise YunohostValidationError(
"global_settings_bad_choice_for_enum",
setting=key,
choice=str(value),
available_choices=", ".join(settings[key]["choices"]),
)
else:
- raise YunohostError(
+ raise YunohostValidationError(
"global_settings_unknown_type", setting=key, unknown_type=key_type
)
@@ -214,7 +239,9 @@ def settings_reset(key):
settings = _get_settings()
if key not in settings:
- raise YunohostError("global_settings_key_doesnt_exists", settings_key=key)
+ raise YunohostValidationError(
+ "global_settings_key_doesnt_exists", settings_key=key
+ )
settings[key]["value"] = settings[key]["default"]
_save_settings(settings)
@@ -253,19 +280,18 @@ def settings_reset_all():
}
+def _get_setting_description(key):
+ return m18n.n("global_settings_setting_%s" % key.replace(".", "_"))
+
+
def _get_settings():
- def get_setting_description(key):
- if key.startswith("example"):
- # (This is for dummy stuff used during unit tests)
- return "Dummy %s setting" % key.split(".")[-1]
- return m18n.n("global_settings_setting_%s" % key.replace(".", "_"))
settings = {}
for key, value in DEFAULTS.copy().items():
settings[key] = value
settings[key]["value"] = value["default"]
- settings[key]["description"] = get_setting_description(key)
+ settings[key]["description"] = _get_setting_description(key)
if not os.path.exists(SETTINGS_PATH):
return settings
@@ -294,7 +320,7 @@ def _get_settings():
for key, value in local_settings.items():
if key in settings:
settings[key] = value
- settings[key]["description"] = get_setting_description(key)
+ settings[key]["description"] = _get_setting_description(key)
else:
logger.warning(
m18n.n(
@@ -304,7 +330,7 @@ def _get_settings():
)
unknown_settings[key] = value
except Exception as e:
- raise YunohostError("global_settings_cant_open_settings", reason=e)
+ raise YunohostValidationError("global_settings_cant_open_settings", reason=e)
if unknown_settings:
try:
@@ -377,18 +403,35 @@ def trigger_post_change_hook(setting_name, old_value, new_value):
# ===========================================
+@post_change_hook("ssowat.panel_overlay.enabled")
+@post_change_hook("security.nginx.redirect_to_https")
@post_change_hook("security.nginx.compatibility")
+@post_change_hook("security.webadmin.allowlist.enabled")
+@post_change_hook("security.webadmin.allowlist")
def reconfigure_nginx(setting_name, old_value, new_value):
if old_value != new_value:
regen_conf(names=["nginx"])
+@post_change_hook("security.experimental.enabled")
+def reconfigure_nginx_and_yunohost(setting_name, old_value, new_value):
+ if old_value != new_value:
+ regen_conf(names=["nginx", "yunohost"])
+
+
@post_change_hook("security.ssh.compatibility")
def reconfigure_ssh(setting_name, old_value, new_value):
if old_value != new_value:
regen_conf(names=["ssh"])
+@post_change_hook("security.ssh.port")
+def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value):
+ if old_value != new_value:
+ regen_conf(names=["ssh", "fail2ban"])
+ firewall_reload()
+
+
@post_change_hook("smtp.allow_ipv6")
@post_change_hook("smtp.relay.host")
@post_change_hook("smtp.relay.port")
diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py
index f7c6fcbb1..ecee39f4a 100644
--- a/src/yunohost/ssh.py
+++ b/src/yunohost/ssh.py
@@ -3,62 +3,17 @@
import re
import os
import pwd
-import subprocess
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostValidationError
from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir
SSHD_CONFIG_PATH = "/etc/ssh/sshd_config"
-def user_ssh_allow(username):
- """
- Allow YunoHost user connect as ssh.
-
- Keyword argument:
- username -- User username
- """
- # TODO it would be good to support different kind of shells
-
- if not _get_user_for_ssh(username):
- raise YunohostError("user_unknown", user=username)
-
- from yunohost.utils.ldap import _get_ldap_interface
-
- ldap = _get_ldap_interface()
- ldap.update("uid=%s,ou=users" % username, {"loginShell": ["/bin/bash"]})
-
- # Somehow this is needed otherwise the PAM thing doesn't forget about the
- # old loginShell value ?
- subprocess.call(["nscd", "-i", "passwd"])
-
-
-def user_ssh_disallow(username):
- """
- Disallow YunoHost user connect as ssh.
-
- Keyword argument:
- username -- User username
- """
- # TODO it would be good to support different kind of shells
-
- if not _get_user_for_ssh(username):
- raise YunohostError("user_unknown", user=username)
-
- from yunohost.utils.ldap import _get_ldap_interface
-
- ldap = _get_ldap_interface()
- ldap.update("uid=%s,ou=users" % username, {"loginShell": ["/bin/false"]})
-
- # Somehow this is needed otherwise the PAM thing doesn't forget about the
- # old loginShell value ?
- subprocess.call(["nscd", "-i", "passwd"])
-
-
def user_ssh_list_keys(username):
user = _get_user_for_ssh(username, ["homeDirectory"])
if not user:
- raise Exception("User with username '%s' doesn't exists" % username)
+ raise YunohostValidationError("user_unknown", user=username)
authorized_keys_file = os.path.join(
user["homeDirectory"][0], ".ssh", "authorized_keys"
@@ -95,7 +50,7 @@ def user_ssh_list_keys(username):
def user_ssh_add_key(username, key, comment):
user = _get_user_for_ssh(username, ["homeDirectory", "uid"])
if not user:
- raise Exception("User with username '%s' doesn't exists" % username)
+ raise YunohostValidationError("user_unknown", user=username)
authorized_keys_file = os.path.join(
user["homeDirectory"][0], ".ssh", "authorized_keys"
@@ -109,6 +64,7 @@ def user_ssh_add_key(username, key, comment):
parents=True,
uid=user["uid"][0],
)
+ chmod(os.path.join(user["homeDirectory"][0], ".ssh"), 0o600)
# create empty file to set good permissions
write_to_file(authorized_keys_file, "")
@@ -135,21 +91,24 @@ def user_ssh_add_key(username, key, comment):
def user_ssh_remove_key(username, key):
user = _get_user_for_ssh(username, ["homeDirectory", "uid"])
if not user:
- raise Exception("User with username '%s' doesn't exists" % username)
+ raise YunohostValidationError("user_unknown", user=username)
authorized_keys_file = os.path.join(
user["homeDirectory"][0], ".ssh", "authorized_keys"
)
if not os.path.exists(authorized_keys_file):
- raise Exception(
- "this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file)
+ raise YunohostValidationError(
+ "this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file),
+ raw_msg=True,
)
authorized_keys_content = read_file(authorized_keys_file)
if key not in authorized_keys_content:
- raise Exception("Key '{}' is not present in authorized_keys".format(key))
+ raise YunohostValidationError(
+ "Key '{}' is not present in authorized_keys".format(key), raw_msg=True
+ )
# don't delete the previous comment because we can't verify if it's legit
@@ -196,8 +155,6 @@ def _get_user_for_ssh(username, attrs=None):
"username": "root",
"fullname": "",
"mail": "",
- "ssh_allowed": ssh_root_login_status()["PermitRootLogin"],
- "shell": root_unix.pw_shell,
"home_path": root_unix.pw_dir,
}
@@ -207,8 +164,6 @@ def _get_user_for_ssh(username, attrs=None):
"username": "admin",
"fullname": "",
"mail": "",
- "ssh_allowed": admin_unix.pw_shell.strip() != "/bin/false",
- "shell": admin_unix.pw_shell,
"home_path": admin_unix.pw_dir,
}
diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py
index 49f87decf..a07c44346 100644
--- a/src/yunohost/tests/conftest.py
+++ b/src/yunohost/tests/conftest.py
@@ -1,14 +1,11 @@
import os
import pytest
-import sys
import moulinette
-from moulinette import m18n, msettings
+from moulinette import m18n, Moulinette
from yunohost.utils.error import YunohostError
from contextlib import contextmanager
-sys.path.append("..")
-
@pytest.fixture(scope="session", autouse=True)
def clone_test_app(request):
@@ -77,8 +74,21 @@ moulinette.core.Moulinette18n.n = new_m18nn
def pytest_cmdline_main(config):
+ import sys
+
sys.path.insert(0, "/usr/lib/moulinette/")
import yunohost
yunohost.init(debug=config.option.yunodebug)
- msettings["interface"] = "test"
+
+ class DummyInterface:
+
+ type = "cli"
+
+ def prompt(self, *args, **kwargs):
+ raise NotImplementedError
+
+ def display(self, message, *args, **kwargs):
+ print(message)
+
+ Moulinette._interface = DummyInterface()
diff --git a/src/yunohost/tests/test_appscatalog.py b/src/yunohost/tests/test_app_catalog.py
similarity index 94%
rename from src/yunohost/tests/test_appscatalog.py
rename to src/yunohost/tests/test_app_catalog.py
index e3bd5d49d..8423b868e 100644
--- a/src/yunohost/tests/test_appscatalog.py
+++ b/src/yunohost/tests/test_app_catalog.py
@@ -9,7 +9,7 @@ from moulinette import m18n
from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml
from yunohost.utils.error import YunohostError
-from yunohost.app import (
+from yunohost.app_catalog import (
_initialize_apps_catalog_system,
_read_apps_catalog_list,
_update_apps_catalog,
@@ -19,13 +19,11 @@ from yunohost.app import (
logger,
APPS_CATALOG_CACHE,
APPS_CATALOG_CONF,
- APPS_CATALOG_CRON_PATH,
APPS_CATALOG_API_VERSION,
APPS_CATALOG_DEFAULT_URL,
)
APPS_CATALOG_DEFAULT_URL_FULL = _actual_apps_catalog_api_url(APPS_CATALOG_DEFAULT_URL)
-CRON_FOLDER, CRON_NAME = APPS_CATALOG_CRON_PATH.rsplit("/", 1)
DUMMY_APP_CATALOG = """{
"apps": {
@@ -50,10 +48,6 @@ def setup_function(function):
# Clear apps catalog cache
shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True)
- # Clear apps_catalog cron
- if os.path.exists(APPS_CATALOG_CRON_PATH):
- os.remove(APPS_CATALOG_CRON_PATH)
-
# Clear apps_catalog conf
if os.path.exists(APPS_CATALOG_CONF):
os.remove(APPS_CATALOG_CONF)
@@ -67,11 +61,6 @@ def teardown_function(function):
shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True)
-def cron_job_is_there():
- r = os.system("run-parts -v --test %s | grep %s" % (CRON_FOLDER, CRON_NAME))
- return r == 0
-
-
#
# ################################################
#
@@ -83,17 +72,12 @@ def test_apps_catalog_init(mocker):
assert not glob.glob(APPS_CATALOG_CACHE + "/*")
# Conf doesn't exist yet
assert not os.path.exists(APPS_CATALOG_CONF)
- # Conf doesn't exist yet
- assert not os.path.exists(APPS_CATALOG_CRON_PATH)
# Initialize ...
mocker.spy(m18n, "n")
_initialize_apps_catalog_system()
m18n.n.assert_any_call("apps_catalog_init_success")
- # Then there's a cron enabled
- assert cron_job_is_there()
-
# And a conf with at least one list
assert os.path.exists(APPS_CATALOG_CONF)
apps_catalog_list = _read_apps_catalog_list()
diff --git a/src/yunohost/tests/test_app_config.py b/src/yunohost/tests/test_app_config.py
new file mode 100644
index 000000000..0eb813672
--- /dev/null
+++ b/src/yunohost/tests/test_app_config.py
@@ -0,0 +1,206 @@
+import glob
+import os
+import shutil
+import pytest
+from mock import patch
+
+from .conftest import get_test_apps_dir
+
+from moulinette import Moulinette
+from moulinette.utils.filesystem import read_file
+
+from yunohost.domain import _get_maindomain
+from yunohost.app import (
+ app_setting,
+ app_install,
+ app_remove,
+ _is_installed,
+ app_config_get,
+ app_config_set,
+ app_ssowatconf,
+)
+
+from yunohost.utils.error import YunohostError, YunohostValidationError
+
+
+def setup_function(function):
+
+ clean()
+
+
+def teardown_function(function):
+
+ clean()
+
+
+def clean():
+
+ # Make sure we have a ssowat
+ os.system("mkdir -p /etc/ssowat/")
+ app_ssowatconf()
+
+ test_apps = ["config_app", "legacy_app"]
+
+ for test_app in test_apps:
+
+ if _is_installed(test_app):
+ app_remove(test_app)
+
+ for filepath in glob.glob("/etc/nginx/conf.d/*.d/*%s*" % test_app):
+ os.remove(filepath)
+ for folderpath in glob.glob("/etc/yunohost/apps/*%s*" % test_app):
+ shutil.rmtree(folderpath, ignore_errors=True)
+ for folderpath in glob.glob("/var/www/*%s*" % test_app):
+ shutil.rmtree(folderpath, ignore_errors=True)
+
+ os.system("bash -c \"mysql -B 2>/dev/null <<< 'DROP DATABASE %s' \"" % test_app)
+ os.system(
+ "bash -c \"mysql -B 2>/dev/null <<< 'DROP USER %s@localhost'\"" % test_app
+ )
+
+ # Reset failed quota for service to avoid running into start-limit rate ?
+ os.system("systemctl reset-failed nginx")
+ os.system("systemctl start nginx")
+
+
+@pytest.fixture()
+def legacy_app(request):
+
+ main_domain = _get_maindomain()
+
+ app_install(
+ os.path.join(get_test_apps_dir(), "legacy_app_ynh"),
+ args="domain=%s&path=%s&is_public=%s" % (main_domain, "/", 1),
+ force=True,
+ )
+
+ def remove_app():
+ app_remove("legacy_app")
+
+ request.addfinalizer(remove_app)
+
+ return "legacy_app"
+
+
+@pytest.fixture()
+def config_app(request):
+
+ app_install(
+ os.path.join(get_test_apps_dir(), "config_app_ynh"),
+ args="",
+ force=True,
+ )
+
+ def remove_app():
+ app_remove("config_app")
+
+ request.addfinalizer(remove_app)
+
+ return "config_app"
+
+
+def test_app_config_get(config_app):
+
+ assert isinstance(app_config_get(config_app), dict)
+ assert isinstance(app_config_get(config_app, full=True), dict)
+ assert isinstance(app_config_get(config_app, export=True), dict)
+ assert isinstance(app_config_get(config_app, "main"), dict)
+ assert isinstance(app_config_get(config_app, "main.components"), dict)
+ assert app_config_get(config_app, "main.components.boolean") == "0"
+
+
+def test_app_config_nopanel(legacy_app):
+
+ with pytest.raises(YunohostValidationError):
+ app_config_get(legacy_app)
+
+
+def test_app_config_get_nonexistentstuff(config_app):
+
+ with pytest.raises(YunohostValidationError):
+ app_config_get("nonexistent")
+
+ with pytest.raises(YunohostValidationError):
+ app_config_get(config_app, "nonexistent")
+
+ with pytest.raises(YunohostValidationError):
+ app_config_get(config_app, "main.nonexistent")
+
+ with pytest.raises(YunohostValidationError):
+ app_config_get(config_app, "main.components.nonexistent")
+
+ app_setting(config_app, "boolean", delete=True)
+ with pytest.raises(YunohostError):
+ app_config_get(config_app, "main.components.boolean")
+
+
+def test_app_config_regular_setting(config_app):
+
+ assert app_config_get(config_app, "main.components.boolean") == "0"
+
+ app_config_set(config_app, "main.components.boolean", "no")
+
+ assert app_config_get(config_app, "main.components.boolean") == "0"
+ assert app_setting(config_app, "boolean") == "0"
+
+ app_config_set(config_app, "main.components.boolean", "yes")
+
+ assert app_config_get(config_app, "main.components.boolean") == "1"
+ assert app_setting(config_app, "boolean") == "1"
+
+ with pytest.raises(YunohostValidationError), patch.object(
+ os, "isatty", return_value=False
+ ), patch.object(Moulinette, "prompt", return_value="pwet"):
+ app_config_set(config_app, "main.components.boolean", "pwet")
+
+
+def test_app_config_bind_on_file(config_app):
+
+ # c.f. conf/test.php in the config app
+ assert '$arg5= "Arg5 value";' in read_file("/var/www/config_app/test.php")
+ assert app_config_get(config_app, "bind.variable.arg5") == "Arg5 value"
+ assert app_setting(config_app, "arg5") is None
+
+ app_config_set(config_app, "bind.variable.arg5", "Foo Bar")
+
+ assert '$arg5= "Foo Bar";' in read_file("/var/www/config_app/test.php")
+ assert app_config_get(config_app, "bind.variable.arg5") == "Foo Bar"
+ assert app_setting(config_app, "arg5") == "Foo Bar"
+
+
+def test_app_config_custom_get(config_app):
+
+ assert app_setting(config_app, "arg9") is None
+ assert (
+ "Files in /var/www"
+ in app_config_get(config_app, "bind.function.arg9")["ask"]["en"]
+ )
+ assert app_setting(config_app, "arg9") is None
+
+
+def test_app_config_custom_validator(config_app):
+
+ # c.f. the config script
+ # arg8 is a password that must be at least 8 chars
+ assert not os.path.exists("/var/www/config_app/password")
+ assert app_setting(config_app, "arg8") is None
+
+ with pytest.raises(YunohostValidationError):
+ app_config_set(config_app, "bind.function.arg8", "pZo6i7u91h")
+
+ assert not os.path.exists("/var/www/config_app/password")
+ assert app_setting(config_app, "arg8") is None
+
+
+def test_app_config_custom_set(config_app):
+
+ assert not os.path.exists("/var/www/config_app/password")
+ assert app_setting(config_app, "arg8") is None
+
+ app_config_set(config_app, "bind.function.arg8", "OneSuperStrongPassword")
+
+ assert os.path.exists("/var/www/config_app/password")
+ content = read_file("/var/www/config_app/password")
+ assert "OneSuperStrongPassword" not in content
+ assert content.startswith("$6$saltsalt$")
+ assert app_setting(config_app, "arg8") is None
diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py
index ae8a4829b..22e18ec9a 100644
--- a/src/yunohost/tests/test_apps.py
+++ b/src/yunohost/tests/test_apps.py
@@ -41,7 +41,13 @@ def clean():
os.system("mkdir -p /etc/ssowat/")
app_ssowatconf()
- test_apps = ["break_yo_system", "legacy_app", "legacy_app__2", "full_domain_app"]
+ test_apps = [
+ "break_yo_system",
+ "legacy_app",
+ "legacy_app__2",
+ "full_domain_app",
+ "my_webapp",
+ ]
for test_app in test_apps:
@@ -55,18 +61,13 @@ def clean():
for folderpath in glob.glob("/var/www/*%s*" % test_app):
shutil.rmtree(folderpath, ignore_errors=True)
+ os.system("bash -c \"mysql -B 2>/dev/null <<< 'DROP DATABASE %s' \"" % test_app)
os.system(
- "bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP DATABASE %s' \""
- % test_app
- )
- os.system(
- "bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP USER %s@localhost'\""
- % test_app
+ "bash -c \"mysql -B 2>/dev/null <<< 'DROP USER %s@localhost'\"" % test_app
)
- os.system(
- "systemctl reset-failed nginx"
- ) # Reset failed quota for service to avoid running into start-limit rate ?
+ # Reset failed quota for service to avoid running into start-limit rate ?
+ os.system("systemctl reset-failed nginx")
os.system("systemctl start nginx")
# Clean permissions
@@ -137,7 +138,7 @@ def app_is_exposed_on_http(domain, path, message_in_page):
try:
r = requests.get(
- "http://127.0.0.1" + path + "/",
+ "https://127.0.0.1" + path + "/",
headers={"Host": domain},
timeout=10,
verify=False,
@@ -194,6 +195,32 @@ def test_legacy_app_install_main_domain():
assert app_is_not_installed(main_domain, "legacy_app")
+def test_app_from_catalog():
+ main_domain = _get_maindomain()
+
+ app_install(
+ "my_webapp",
+ args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0",
+ )
+ app_map_ = app_map(raw=True)
+ assert main_domain in app_map_
+ assert "/site" in app_map_[main_domain]
+ assert "id" in app_map_[main_domain]["/site"]
+ assert app_map_[main_domain]["/site"]["id"] == "my_webapp"
+
+ assert app_is_installed(main_domain, "my_webapp")
+ assert app_is_exposed_on_http(main_domain, "/site", "Custom Web App")
+
+ # Try upgrade, should do nothing
+ app_upgrade("my_webapp")
+ # Force upgrade, should upgrade to the same version
+ app_upgrade("my_webapp", force=True)
+
+ app_remove("my_webapp")
+
+ assert app_is_not_installed(main_domain, "my_webapp")
+
+
def test_legacy_app_install_secondary_domain(secondary_domain):
install_legacy_app(secondary_domain, "/legacy")
diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py
deleted file mode 100644
index 98dd280ff..000000000
--- a/src/yunohost/tests/test_apps_arguments_parsing.py
+++ /dev/null
@@ -1,1637 +0,0 @@
-import sys
-import pytest
-
-from mock import patch
-from io import StringIO
-from collections import OrderedDict
-
-from moulinette import msignals
-
-from yunohost import domain, user
-from yunohost.app import _parse_args_in_yunohost_format, PasswordArgumentParser
-from yunohost.utils.error import YunohostError
-
-
-"""
-Argument default format:
-{
- "name": "the_name",
- "type": "one_of_the_available_type", // "sting" is not specified
- "ask": {
- "en": "the question in english",
- "fr": "the question in french"
- },
- "help": {
- "en": "some help text in english",
- "fr": "some help text in french"
- },
- "example": "an example value", // optional
- "default", "some stuff", // optional, not available for all types
- "optional": true // optional, will skip if not answered
-}
-
-User answers:
-{"name": "value", ...}
-"""
-
-
-def test_parse_args_in_yunohost_format_empty():
- assert _parse_args_in_yunohost_format({}, []) == {}
-
-
-def test_parse_args_in_yunohost_format_string():
- questions = [
- {
- "name": "some_string",
- "type": "string",
- }
- ]
- answers = {"some_string": "some_value"}
- expected_result = OrderedDict({"some_string": ("some_value", "string")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_default_type():
- questions = [
- {
- "name": "some_string",
- }
- ]
- answers = {"some_string": "some_value"}
- expected_result = OrderedDict({"some_string": ("some_value", "string")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_no_input():
- questions = [
- {
- "name": "some_string",
- }
- ]
- answers = {}
-
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_string_input():
- questions = [
- {
- "name": "some_string",
- "ask": "some question",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("some_value", "string")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_input_no_ask():
- questions = [
- {
- "name": "some_string",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("some_value", "string")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_no_input_optional():
- questions = [
- {
- "name": "some_string",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("", "string")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_optional_with_input():
- questions = [
- {
- "name": "some_string",
- "ask": "some question",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("some_value", "string")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_optional_with_empty_input():
- questions = [
- {
- "name": "some_string",
- "ask": "some question",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("", "string")})
-
- with patch.object(msignals, "prompt", return_value=""):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask():
- questions = [
- {
- "name": "some_string",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("some_value", "string")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_no_input_default():
- questions = [
- {
- "name": "some_string",
- "ask": "some question",
- "default": "some_value",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("some_value", "string")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_input_test_ask():
- ask_text = "some question"
- questions = [
- {
- "name": "some_string",
- "ask": ask_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with(ask_text, False)
-
-
-def test_parse_args_in_yunohost_format_string_input_test_ask_with_default():
- ask_text = "some question"
- default_text = "some example"
- questions = [
- {
- "name": "some_string",
- "ask": ask_text,
- "default": default_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False)
-
-
-@pytest.mark.skip # we should do something with this example
-def test_parse_args_in_yunohost_format_string_input_test_ask_with_example():
- ask_text = "some question"
- example_text = "some example"
- questions = [
- {
- "name": "some_string",
- "ask": ask_text,
- "example": example_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert example_text in prompt.call_args[0][0]
-
-
-@pytest.mark.skip # we should do something with this help
-def test_parse_args_in_yunohost_format_string_input_test_ask_with_help():
- ask_text = "some question"
- help_text = "some_help"
- questions = [
- {
- "name": "some_string",
- "ask": ask_text,
- "help": help_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert help_text in prompt.call_args[0][0]
-
-
-def test_parse_args_in_yunohost_format_string_with_choice():
- questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}]
- answers = {"some_string": "fr"}
- expected_result = OrderedDict({"some_string": ("fr", "string")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_with_choice_prompt():
- questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}]
- answers = {"some_string": "fr"}
- expected_result = OrderedDict({"some_string": ("fr", "string")})
- with patch.object(msignals, "prompt", return_value="fr"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_string_with_choice_bad():
- questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}]
- answers = {"some_string": "bad"}
-
- with pytest.raises(YunohostError):
- assert _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_string_with_choice_ask():
- ask_text = "some question"
- choices = ["fr", "en", "es", "it", "ru"]
- questions = [
- {
- "name": "some_string",
- "ask": ask_text,
- "choices": choices,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="ru") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
-
- for choice in choices:
- assert choice in prompt.call_args[0][0]
-
-
-def test_parse_args_in_yunohost_format_string_with_choice_default():
- questions = [
- {
- "name": "some_string",
- "type": "string",
- "choices": ["fr", "en"],
- "default": "en",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_string": ("en", "string")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- }
- ]
- answers = {"some_password": "some_value"}
- expected_result = OrderedDict({"some_password": ("some_value", "password")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password_no_input():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- }
- ]
- answers = {}
-
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_password_input():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": "some question",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_password": ("some_value", "password")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password_input_no_ask():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_password": ("some_value", "password")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password_no_input_optional():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_password": ("", "password")})
-
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
- questions = [
- {"name": "some_password", "type": "password", "optional": True, "default": ""}
- ]
-
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password_optional_with_input():
- questions = [
- {
- "name": "some_password",
- "ask": "some question",
- "type": "password",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_password": ("some_value", "password")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password_optional_with_empty_input():
- questions = [
- {
- "name": "some_password",
- "ask": "some question",
- "type": "password",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_password": ("", "password")})
-
- with patch.object(msignals, "prompt", return_value=""):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_password": ("some_value", "password")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_password_no_input_default():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": "some question",
- "default": "some_value",
- }
- ]
- answers = {}
-
- # no default for password!
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-@pytest.mark.skip # this should raises
-def test_parse_args_in_yunohost_format_password_no_input_example():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": "some question",
- "example": "some_value",
- }
- ]
- answers = {"some_password": "some_value"}
-
- # no example for password!
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_password_input_test_ask():
- ask_text = "some question"
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": ask_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with(ask_text, True)
-
-
-@pytest.mark.skip # we should do something with this example
-def test_parse_args_in_yunohost_format_password_input_test_ask_with_example():
- ask_text = "some question"
- example_text = "some example"
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": ask_text,
- "example": example_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert example_text in prompt.call_args[0][0]
-
-
-@pytest.mark.skip # we should do something with this help
-def test_parse_args_in_yunohost_format_password_input_test_ask_with_help():
- ask_text = "some question"
- help_text = "some_help"
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": ask_text,
- "help": help_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert help_text in prompt.call_args[0][0]
-
-
-def test_parse_args_in_yunohost_format_password_bad_chars():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": "some question",
- "example": "some_value",
- }
- ]
-
- for i in PasswordArgumentParser.forbidden_chars:
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format({"some_password": i * 8}, questions)
-
-
-def test_parse_args_in_yunohost_format_password_strong_enough():
- questions = [
- {
- "name": "some_password",
- "type": "password",
- "ask": "some question",
- "example": "some_value",
- }
- ]
-
- with pytest.raises(YunohostError):
- # too short
- _parse_args_in_yunohost_format({"some_password": "a"}, questions)
-
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format({"some_password": "password"}, questions)
-
-
-def test_parse_args_in_yunohost_format_password_optional_strong_enough():
- questions = [
- {
- "name": "some_password",
- "ask": "some question",
- "type": "password",
- "optional": True,
- }
- ]
-
- with pytest.raises(YunohostError):
- # too short
- _parse_args_in_yunohost_format({"some_password": "a"}, questions)
-
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format({"some_password": "password"}, questions)
-
-
-def test_parse_args_in_yunohost_format_path():
- questions = [
- {
- "name": "some_path",
- "type": "path",
- }
- ]
- answers = {"some_path": "some_value"}
- expected_result = OrderedDict({"some_path": ("some_value", "path")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_no_input():
- questions = [
- {
- "name": "some_path",
- "type": "path",
- }
- ]
- answers = {}
-
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_path_input():
- questions = [
- {
- "name": "some_path",
- "type": "path",
- "ask": "some question",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_path": ("some_value", "path")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_input_no_ask():
- questions = [
- {
- "name": "some_path",
- "type": "path",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_path": ("some_value", "path")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_no_input_optional():
- questions = [
- {
- "name": "some_path",
- "type": "path",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_path": ("", "path")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_optional_with_input():
- questions = [
- {
- "name": "some_path",
- "ask": "some question",
- "type": "path",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_path": ("some_value", "path")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_optional_with_empty_input():
- questions = [
- {
- "name": "some_path",
- "ask": "some question",
- "type": "path",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_path": ("", "path")})
-
- with patch.object(msignals, "prompt", return_value=""):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_optional_with_input_without_ask():
- questions = [
- {
- "name": "some_path",
- "type": "path",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_path": ("some_value", "path")})
-
- with patch.object(msignals, "prompt", return_value="some_value"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_no_input_default():
- questions = [
- {
- "name": "some_path",
- "ask": "some question",
- "type": "path",
- "default": "some_value",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_path": ("some_value", "path")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_path_input_test_ask():
- ask_text = "some question"
- questions = [
- {
- "name": "some_path",
- "type": "path",
- "ask": ask_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with(ask_text, False)
-
-
-def test_parse_args_in_yunohost_format_path_input_test_ask_with_default():
- ask_text = "some question"
- default_text = "some example"
- questions = [
- {
- "name": "some_path",
- "type": "path",
- "ask": ask_text,
- "default": default_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False)
-
-
-@pytest.mark.skip # we should do something with this example
-def test_parse_args_in_yunohost_format_path_input_test_ask_with_example():
- ask_text = "some question"
- example_text = "some example"
- questions = [
- {
- "name": "some_path",
- "type": "path",
- "ask": ask_text,
- "example": example_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert example_text in prompt.call_args[0][0]
-
-
-@pytest.mark.skip # we should do something with this help
-def test_parse_args_in_yunohost_format_path_input_test_ask_with_help():
- ask_text = "some question"
- help_text = "some_help"
- questions = [
- {
- "name": "some_path",
- "type": "path",
- "ask": ask_text,
- "help": help_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="some_value") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert help_text in prompt.call_args[0][0]
-
-
-def test_parse_args_in_yunohost_format_boolean():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- }
- ]
- answers = {"some_boolean": "y"}
- expected_result = OrderedDict({"some_boolean": (1, "boolean")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_all_yes():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- }
- ]
- expected_result = OrderedDict({"some_boolean": (1, "boolean")})
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "y"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "Y"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "yes"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "Yes"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "YES"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "1"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": 1}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": True}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "True"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "TRUE"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "true"}, questions)
- == expected_result
- )
-
-
-def test_parse_args_in_yunohost_format_boolean_all_no():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- }
- ]
- expected_result = OrderedDict({"some_boolean": (0, "boolean")})
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "n"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "N"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "no"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "No"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "No"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "0"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": 0}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": False}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "False"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "FALSE"}, questions)
- == expected_result
- )
- assert (
- _parse_args_in_yunohost_format({"some_boolean": "false"}, questions)
- == expected_result
- )
-
-
-# XXX apparently boolean are always False (0) by default, I'm not sure what to think about that
-def test_parse_args_in_yunohost_format_boolean_no_input():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- }
- ]
- answers = {}
-
- expected_result = OrderedDict({"some_boolean": (0, "boolean")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_bad_input():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- }
- ]
- answers = {"some_boolean": "stuff"}
-
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_boolean_input():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- "ask": "some question",
- }
- ]
- answers = {}
-
- expected_result = OrderedDict({"some_boolean": (1, "boolean")})
- with patch.object(msignals, "prompt", return_value="y"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
- expected_result = OrderedDict({"some_boolean": (0, "boolean")})
- with patch.object(msignals, "prompt", return_value="n"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_input_no_ask():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_boolean": (1, "boolean")})
-
- with patch.object(msignals, "prompt", return_value="y"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_no_input_optional():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_optional_with_input():
- questions = [
- {
- "name": "some_boolean",
- "ask": "some question",
- "type": "boolean",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_boolean": (1, "boolean")})
-
- with patch.object(msignals, "prompt", return_value="y"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_optional_with_empty_input():
- questions = [
- {
- "name": "some_boolean",
- "ask": "some question",
- "type": "boolean",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false
-
- with patch.object(msignals, "prompt", return_value=""):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_optional_with_input_without_ask():
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_boolean": (0, "boolean")})
-
- with patch.object(msignals, "prompt", return_value="n"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_no_input_default():
- questions = [
- {
- "name": "some_boolean",
- "ask": "some question",
- "type": "boolean",
- "default": 0,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_boolean": (0, "boolean")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_boolean_bad_default():
- questions = [
- {
- "name": "some_boolean",
- "ask": "some question",
- "type": "boolean",
- "default": "bad default",
- }
- ]
- answers = {}
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_boolean_input_test_ask():
- ask_text = "some question"
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- "ask": ask_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value=0) as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with(ask_text + " [yes | no] (default: no)", False)
-
-
-def test_parse_args_in_yunohost_format_boolean_input_test_ask_with_default():
- ask_text = "some question"
- default_text = 1
- questions = [
- {
- "name": "some_boolean",
- "type": "boolean",
- "ask": ask_text,
- "default": default_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value=1) as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with("%s [yes | no] (default: yes)" % ask_text, False)
-
-
-def test_parse_args_in_yunohost_format_domain_empty():
- questions = [
- {
- "name": "some_domain",
- "type": "domain",
- }
- ]
- main_domain = "my_main_domain.com"
- expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
- answers = {}
-
- with patch.object(
- domain, "_get_maindomain", return_value="my_main_domain.com"
- ), patch.object(domain, "domain_list", return_value={"domains": [main_domain]}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_domain():
- main_domain = "my_main_domain.com"
- domains = [main_domain]
- questions = [
- {
- "name": "some_domain",
- "type": "domain",
- }
- ]
-
- answers = {"some_domain": main_domain}
- expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
-
- with patch.object(
- domain, "_get_maindomain", return_value=main_domain
- ), patch.object(domain, "domain_list", return_value={"domains": domains}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_domain_two_domains():
- main_domain = "my_main_domain.com"
- other_domain = "some_other_domain.tld"
- domains = [main_domain, other_domain]
-
- questions = [
- {
- "name": "some_domain",
- "type": "domain",
- }
- ]
- answers = {"some_domain": other_domain}
- expected_result = OrderedDict({"some_domain": (other_domain, "domain")})
-
- with patch.object(
- domain, "_get_maindomain", return_value=main_domain
- ), patch.object(domain, "domain_list", return_value={"domains": domains}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
- answers = {"some_domain": main_domain}
- expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
-
- with patch.object(
- domain, "_get_maindomain", return_value=main_domain
- ), patch.object(domain, "domain_list", return_value={"domains": domains}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_domain_two_domains_wrong_answer():
- main_domain = "my_main_domain.com"
- other_domain = "some_other_domain.tld"
- domains = [main_domain, other_domain]
-
- questions = [
- {
- "name": "some_domain",
- "type": "domain",
- }
- ]
- answers = {"some_domain": "doesnt_exist.pouet"}
-
- with patch.object(
- domain, "_get_maindomain", return_value=main_domain
- ), patch.object(domain, "domain_list", return_value={"domains": domains}):
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_domain_two_domains_default_no_ask():
- main_domain = "my_main_domain.com"
- other_domain = "some_other_domain.tld"
- domains = [main_domain, other_domain]
-
- questions = [
- {
- "name": "some_domain",
- "type": "domain",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
-
- with patch.object(
- domain, "_get_maindomain", return_value=main_domain
- ), patch.object(domain, "domain_list", return_value={"domains": domains}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_domain_two_domains_default():
- main_domain = "my_main_domain.com"
- other_domain = "some_other_domain.tld"
- domains = [main_domain, other_domain]
-
- questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}]
- answers = {}
- expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
-
- with patch.object(
- domain, "_get_maindomain", return_value=main_domain
- ), patch.object(domain, "domain_list", return_value={"domains": domains}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_domain_two_domains_default_input():
- main_domain = "my_main_domain.com"
- other_domain = "some_other_domain.tld"
- domains = [main_domain, other_domain]
-
- questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}]
- answers = {}
-
- with patch.object(
- domain, "_get_maindomain", return_value=main_domain
- ), patch.object(domain, "domain_list", return_value={"domains": domains}):
- expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
- with patch.object(msignals, "prompt", return_value=main_domain):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
- expected_result = OrderedDict({"some_domain": (other_domain, "domain")})
- with patch.object(msignals, "prompt", return_value=other_domain):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_user_empty():
- users = {
- "some_user": {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "p@ynh.local",
- "fullname": "the first name the last name",
- }
- }
-
- questions = [
- {
- "name": "some_user",
- "type": "user",
- }
- ]
- answers = {}
-
- with patch.object(user, "user_list", return_value={"users": users}):
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_user():
- username = "some_user"
- users = {
- username: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "p@ynh.local",
- "fullname": "the first name the last name",
- }
- }
-
- questions = [
- {
- "name": "some_user",
- "type": "user",
- }
- ]
- answers = {"some_user": username}
-
- expected_result = OrderedDict({"some_user": (username, "user")})
-
- with patch.object(user, "user_list", return_value={"users": users}):
- with patch.object(user, "user_info", return_value={}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_user_two_users():
- username = "some_user"
- other_user = "some_other_user"
- users = {
- username: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "p@ynh.local",
- "fullname": "the first name the last name",
- },
- other_user: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "z@ynh.local",
- "fullname": "john doe",
- },
- }
-
- questions = [
- {
- "name": "some_user",
- "type": "user",
- }
- ]
- answers = {"some_user": other_user}
- expected_result = OrderedDict({"some_user": (other_user, "user")})
-
- with patch.object(user, "user_list", return_value={"users": users}):
- with patch.object(user, "user_info", return_value={}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
- answers = {"some_user": username}
- expected_result = OrderedDict({"some_user": (username, "user")})
-
- with patch.object(user, "user_list", return_value={"users": users}):
- with patch.object(user, "user_info", return_value={}):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_user_two_users_wrong_answer():
- username = "my_username.com"
- other_user = "some_other_user"
- users = {
- username: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "p@ynh.local",
- "fullname": "the first name the last name",
- },
- other_user: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "z@ynh.local",
- "fullname": "john doe",
- },
- }
-
- questions = [
- {
- "name": "some_user",
- "type": "user",
- }
- ]
- answers = {"some_user": "doesnt_exist.pouet"}
-
- with patch.object(user, "user_list", return_value={"users": users}):
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_user_two_users_no_default():
- username = "my_username.com"
- other_user = "some_other_user.tld"
- users = {
- username: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "p@ynh.local",
- "fullname": "the first name the last name",
- },
- other_user: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "z@ynh.local",
- "fullname": "john doe",
- },
- }
-
- questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}]
- answers = {}
-
- with patch.object(user, "user_list", return_value={"users": users}):
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_user_two_users_default_input():
- username = "my_username.com"
- other_user = "some_other_user.tld"
- users = {
- username: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "p@ynh.local",
- "fullname": "the first name the last name",
- },
- other_user: {
- "ssh_allowed": False,
- "username": "some_user",
- "shell": "/bin/false",
- "mailbox-quota": "0",
- "mail": "z@ynh.local",
- "fullname": "john doe",
- },
- }
-
- questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}]
- answers = {}
-
- with patch.object(user, "user_list", return_value={"users": users}):
- with patch.object(user, "user_info", return_value={}):
- expected_result = OrderedDict({"some_user": (username, "user")})
- with patch.object(msignals, "prompt", return_value=username):
- assert (
- _parse_args_in_yunohost_format(answers, questions)
- == expected_result
- )
-
- expected_result = OrderedDict({"some_user": (other_user, "user")})
- with patch.object(msignals, "prompt", return_value=other_user):
- assert (
- _parse_args_in_yunohost_format(answers, questions)
- == expected_result
- )
-
-
-def test_parse_args_in_yunohost_format_number():
- questions = [
- {
- "name": "some_number",
- "type": "number",
- }
- ]
- answers = {"some_number": 1337}
- expected_result = OrderedDict({"some_number": (1337, "number")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_no_input():
- questions = [
- {
- "name": "some_number",
- "type": "number",
- }
- ]
- answers = {}
-
- expected_result = OrderedDict({"some_number": (0, "number")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_bad_input():
- questions = [
- {
- "name": "some_number",
- "type": "number",
- }
- ]
- answers = {"some_number": "stuff"}
-
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
- answers = {"some_number": 1.5}
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_number_input():
- questions = [
- {
- "name": "some_number",
- "type": "number",
- "ask": "some question",
- }
- ]
- answers = {}
-
- expected_result = OrderedDict({"some_number": (1337, "number")})
- with patch.object(msignals, "prompt", return_value="1337"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
- with patch.object(msignals, "prompt", return_value=1337):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
- expected_result = OrderedDict({"some_number": (0, "number")})
- with patch.object(msignals, "prompt", return_value=""):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_input_no_ask():
- questions = [
- {
- "name": "some_number",
- "type": "number",
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_number": (1337, "number")})
-
- with patch.object(msignals, "prompt", return_value="1337"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_no_input_optional():
- questions = [
- {
- "name": "some_number",
- "type": "number",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_number": (0, "number")}) # default to 0
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_optional_with_input():
- questions = [
- {
- "name": "some_number",
- "ask": "some question",
- "type": "number",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_number": (1337, "number")})
-
- with patch.object(msignals, "prompt", return_value="1337"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_optional_with_input_without_ask():
- questions = [
- {
- "name": "some_number",
- "type": "number",
- "optional": True,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_number": (0, "number")})
-
- with patch.object(msignals, "prompt", return_value="0"):
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_no_input_default():
- questions = [
- {
- "name": "some_number",
- "ask": "some question",
- "type": "number",
- "default": 1337,
- }
- ]
- answers = {}
- expected_result = OrderedDict({"some_number": (1337, "number")})
- assert _parse_args_in_yunohost_format(answers, questions) == expected_result
-
-
-def test_parse_args_in_yunohost_format_number_bad_default():
- questions = [
- {
- "name": "some_number",
- "ask": "some question",
- "type": "number",
- "default": "bad default",
- }
- ]
- answers = {}
- with pytest.raises(YunohostError):
- _parse_args_in_yunohost_format(answers, questions)
-
-
-def test_parse_args_in_yunohost_format_number_input_test_ask():
- ask_text = "some question"
- questions = [
- {
- "name": "some_number",
- "type": "number",
- "ask": ask_text,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="1111") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with("%s (default: 0)" % (ask_text), False)
-
-
-def test_parse_args_in_yunohost_format_number_input_test_ask_with_default():
- ask_text = "some question"
- default_value = 1337
- questions = [
- {
- "name": "some_number",
- "type": "number",
- "ask": ask_text,
- "default": default_value,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="1111") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- prompt.assert_called_with("%s (default: %s)" % (ask_text, default_value), False)
-
-
-@pytest.mark.skip # we should do something with this example
-def test_parse_args_in_yunohost_format_number_input_test_ask_with_example():
- ask_text = "some question"
- example_value = 1337
- questions = [
- {
- "name": "some_number",
- "type": "number",
- "ask": ask_text,
- "example": example_value,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="1111") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert example_value in prompt.call_args[0][0]
-
-
-@pytest.mark.skip # we should do something with this help
-def test_parse_args_in_yunohost_format_number_input_test_ask_with_help():
- ask_text = "some question"
- help_value = 1337
- questions = [
- {
- "name": "some_number",
- "type": "number",
- "ask": ask_text,
- "help": help_value,
- }
- ]
- answers = {}
-
- with patch.object(msignals, "prompt", return_value="1111") as prompt:
- _parse_args_in_yunohost_format(answers, questions)
- assert ask_text in prompt.call_args[0][0]
- assert help_value in prompt.call_args[0][0]
-
-
-def test_parse_args_in_yunohost_format_display_text():
- questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}]
- answers = {}
-
- with patch.object(sys, "stdout", new_callable=StringIO) as stdout:
- _parse_args_in_yunohost_format(answers, questions)
- assert "foobar" in stdout.getvalue()
diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py
index f15ed391f..7b4c6e2e3 100644
--- a/src/yunohost/tests/test_appurl.py
+++ b/src/yunohost/tests/test_appurl.py
@@ -4,7 +4,12 @@ import os
from .conftest import get_test_apps_dir
from yunohost.utils.error import YunohostError
-from yunohost.app import app_install, app_remove, _normalize_domain_path
+from yunohost.app import (
+ app_install,
+ app_remove,
+ _is_app_repo_url,
+ _parse_app_instance_name,
+)
from yunohost.domain import _get_maindomain, domain_url_available
from yunohost.permission import _validate_and_sanitize_permission_url
@@ -28,20 +33,55 @@ def teardown_function(function):
pass
-def test_normalize_domain_path():
+def test_parse_app_instance_name():
- assert _normalize_domain_path("https://yolo.swag/", "macnuggets") == (
- "yolo.swag",
- "/macnuggets",
+ assert _parse_app_instance_name("yolo") == ("yolo", 1)
+ assert _parse_app_instance_name("yolo1") == ("yolo1", 1)
+ assert _parse_app_instance_name("yolo__0") == ("yolo__0", 1)
+ assert _parse_app_instance_name("yolo__1") == ("yolo", 1)
+ assert _parse_app_instance_name("yolo__23") == ("yolo", 23)
+ assert _parse_app_instance_name("yolo__42__72") == ("yolo__42", 72)
+ assert _parse_app_instance_name("yolo__23qdqsd") == ("yolo__23qdqsd", 1)
+ assert _parse_app_instance_name("yolo__23qdqsd56") == ("yolo__23qdqsd56", 1)
+
+
+def test_repo_url_definition():
+ assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh")
+ assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/")
+ assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh.git")
+ assert _is_app_repo_url(
+ "https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing"
)
- assert _normalize_domain_path("http://yolo.swag", "/macnuggets/") == (
- "yolo.swag",
- "/macnuggets",
+ assert _is_app_repo_url(
+ "https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing/"
)
- assert _normalize_domain_path("yolo.swag/", "macnuggets/") == (
- "yolo.swag",
- "/macnuggets",
+ assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo-bar-123_ynh")
+ assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo_bar_123_ynh")
+ assert _is_app_repo_url("https://github.com/YunoHost-Apps/FooBar123_ynh")
+ assert _is_app_repo_url("https://github.com/labriqueinternet/vpnclient_ynh")
+ assert _is_app_repo_url("https://framagit.org/YunoHost/apps/nodebb_ynh")
+ assert _is_app_repo_url(
+ "https://framagit.org/YunoHost/apps/nodebb_ynh/-/tree/testing"
)
+ assert _is_app_repo_url("https://gitlab.com/yunohost-apps/foobar_ynh")
+ assert _is_app_repo_url("https://code.antopie.org/miraty/qr_ynh")
+ assert _is_app_repo_url(
+ "https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable"
+ )
+ assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh/tree/1.23.4")
+ assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git")
+
+ assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh")
+ assert not _is_app_repo_url("http://github.com/YunoHost-Apps/foobar_ynh")
+ assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_wat")
+ assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat")
+ assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/testing")
+ assert not _is_app_repo_url(
+ "https://github.com/YunoHost-Apps/foobar_ynh_wat/tree/testing"
+ )
+ assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/")
+ assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet")
+ assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet_foo")
def test_urlavailable():
diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py
index 021566544..6e2c3b514 100644
--- a/src/yunohost/tests/test_backuprestore.py
+++ b/src/yunohost/tests/test_backuprestore.py
@@ -2,6 +2,7 @@ import pytest
import os
import shutil
import subprocess
+from mock import patch
from .conftest import message, raiseYunohostError, get_test_apps_dir
@@ -47,8 +48,8 @@ def setup_function(function):
for m in function.__dict__.get("pytestmark", [])
}
- if "with_wordpress_archive_from_2p4" in markers:
- add_archive_wordpress_from_2p4()
+ if "with_wordpress_archive_from_3p8" in markers:
+ add_archive_wordpress_from_3p8()
assert len(backup_list()["archives"]) == 1
if "with_legacy_app_installed" in markers:
@@ -70,14 +71,15 @@ def setup_function(function):
)
assert app_is_installed("backup_recommended_app")
- if "with_system_archive_from_2p4" in markers:
- add_archive_system_from_2p4()
+ if "with_system_archive_from_3p8" in markers:
+ add_archive_system_from_3p8()
assert len(backup_list()["archives"]) == 1
if "with_permission_app_installed" in markers:
assert not app_is_installed("permissions_app")
user_create("alice", "Alice", "White", maindomain, "test123Ynh")
- install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice")
+ with patch.object(os, "isatty", return_value=False):
+ install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice")
assert app_is_installed("permissions_app")
if "with_custom_domain" in markers:
@@ -107,7 +109,8 @@ def teardown_function(function):
if "with_custom_domain" in markers:
domain = markers["with_custom_domain"]["args"][0]
- domain_remove(domain)
+ if domain != maindomain:
+ domain_remove(domain)
@pytest.fixture(autouse=True)
@@ -147,7 +150,7 @@ def backup_test_dependencies_are_met():
# Dummy test apps (or backup archives)
assert os.path.exists(
- os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4")
+ os.path.join(get_test_apps_dir(), "backup_wordpress_from_3p8")
)
assert os.path.exists(os.path.join(get_test_apps_dir(), "legacy_app_ynh"))
assert os.path.exists(
@@ -216,39 +219,25 @@ def install_app(app, path, additionnal_args=""):
)
-def add_archive_wordpress_from_2p4():
+def add_archive_wordpress_from_3p8():
os.system("mkdir -p /home/yunohost.backup/archives")
os.system(
"cp "
- + os.path.join(
- get_test_apps_dir(), "backup_wordpress_from_2p4/backup.info.json"
- )
- + " /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json"
- )
-
- os.system(
- "cp "
- + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.tar.gz")
- + " /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz"
+ + os.path.join(get_test_apps_dir(), "backup_wordpress_from_3p8/backup.tar.gz")
+ + " /home/yunohost.backup/archives/backup_wordpress_from_3p8.tar.gz"
)
-def add_archive_system_from_2p4():
+def add_archive_system_from_3p8():
os.system("mkdir -p /home/yunohost.backup/archives")
os.system(
"cp "
- + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.info.json")
- + " /home/yunohost.backup/archives/backup_system_from_2p4.info.json"
- )
-
- os.system(
- "cp "
- + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.tar.gz")
- + " /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz"
+ + os.path.join(get_test_apps_dir(), "backup_system_from_3p8/backup.tar.gz")
+ + " /home/yunohost.backup/archives/backup_system_from_3p8.tar.gz"
)
@@ -314,12 +303,12 @@ def test_backup_and_restore_all_sys(mocker):
#
-# System restore from 2.4 #
+# System restore from 3.8 #
#
-@pytest.mark.with_system_archive_from_2p4
-def test_restore_system_from_Ynh2p4(monkeypatch, mocker):
+@pytest.mark.with_system_archive_from_3p8
+def test_restore_system_from_Ynh3p8(monkeypatch, mocker):
# Backup current system
with message(mocker, "backup_created"):
@@ -327,7 +316,7 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker):
archives = backup_list()["archives"]
assert len(archives) == 2
- # Restore system archive from 2.4
+ # Restore system archive from 3.8
try:
with message(mocker, "restore_complete"):
backup_restore(
@@ -426,7 +415,7 @@ def test_backup_with_different_output_directory(mocker):
# Create the backup
with message(mocker, "backup_created"):
backup_create(
- system=["conf_ssh"],
+ system=["conf_ynh_settings"],
apps=None,
output_directory="/opt/test_backup_output_directory",
name="backup",
@@ -440,7 +429,7 @@ def test_backup_with_different_output_directory(mocker):
archives_info = backup_info(archives[0], with_details=True)
assert archives_info["apps"] == {}
assert len(archives_info["system"].keys()) == 1
- assert "conf_ssh" in archives_info["system"].keys()
+ assert "conf_ynh_settings" in archives_info["system"].keys()
@pytest.mark.clean_opt_dir
@@ -449,7 +438,7 @@ def test_backup_using_copy_method(mocker):
# Create the backup
with message(mocker, "backup_created"):
backup_create(
- system=["conf_nginx"],
+ system=["conf_ynh_settings"],
apps=None,
output_directory="/opt/test_backup_output_directory",
methods=["copy"],
@@ -464,9 +453,9 @@ def test_backup_using_copy_method(mocker):
#
-@pytest.mark.with_wordpress_archive_from_2p4
+@pytest.mark.with_wordpress_archive_from_3p8
@pytest.mark.with_custom_domain("yolo.test")
-def test_restore_app_wordpress_from_Ynh2p4(mocker):
+def test_restore_app_wordpress_from_Ynh3p8(mocker):
with message(mocker, "restore_complete"):
backup_restore(
@@ -474,19 +463,19 @@ def test_restore_app_wordpress_from_Ynh2p4(mocker):
)
-@pytest.mark.with_wordpress_archive_from_2p4
+@pytest.mark.with_wordpress_archive_from_3p8
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_script_failure_handling(monkeypatch, mocker):
def custom_hook_exec(name, *args, **kwargs):
if os.path.basename(name).startswith("restore"):
monkeypatch.undo()
- raise Exception
+ return (1, None)
- monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
+ monkeypatch.setattr("yunohost.hook.hook_exec", custom_hook_exec)
assert not _is_installed("wordpress")
- with message(mocker, "restore_app_failed", app="wordpress"):
+ with message(mocker, "app_restore_script_failed"):
with raiseYunohostError(mocker, "restore_nothings_done"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
@@ -495,7 +484,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker):
assert not _is_installed("wordpress")
-@pytest.mark.with_wordpress_archive_from_2p4
+@pytest.mark.with_wordpress_archive_from_3p8
def test_restore_app_not_enough_free_space(monkeypatch, mocker):
def custom_free_space_in_directory(dirpath):
return 0
@@ -514,7 +503,7 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker):
assert not _is_installed("wordpress")
-@pytest.mark.with_wordpress_archive_from_2p4
+@pytest.mark.with_wordpress_archive_from_3p8
def test_restore_app_not_in_backup(mocker):
assert not _is_installed("wordpress")
@@ -530,7 +519,7 @@ def test_restore_app_not_in_backup(mocker):
assert not _is_installed("yoloswag")
-@pytest.mark.with_wordpress_archive_from_2p4
+@pytest.mark.with_wordpress_archive_from_3p8
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_already_installed(mocker):
@@ -648,18 +637,18 @@ def test_restore_archive_with_no_json(mocker):
backup_restore(name="badbackup", force=True)
-@pytest.mark.with_wordpress_archive_from_2p4
+@pytest.mark.with_wordpress_archive_from_3p8
def test_restore_archive_with_bad_archive(mocker):
# Break the archive
os.system(
- "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz"
+ "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_3p8.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_3p8_bad.tar.gz"
)
- assert "backup_wordpress_from_2p4" in backup_list()["archives"]
+ assert "backup_wordpress_from_3p8_bad" in backup_list()["archives"]
- with raiseYunohostError(mocker, "backup_archive_open_failed"):
- backup_restore(name="backup_wordpress_from_2p4", force=True)
+ with raiseYunohostError(mocker, "backup_archive_corrupted"):
+ backup_restore(name="backup_wordpress_from_3p8_bad", force=True)
clean_tmp_backup_directory()
@@ -688,9 +677,9 @@ def test_backup_binds_are_readonly(mocker, monkeypatch):
def custom_mount_and_backup(self):
self._organize_files()
- confssh = os.path.join(self.work_dir, "conf/ssh")
+ conf = os.path.join(self.work_dir, "conf/ynh/dkim")
output = subprocess.check_output(
- "touch %s/test 2>&1 || true" % confssh,
+ "touch %s/test 2>&1 || true" % conf,
shell=True,
env={"LANG": "en_US.UTF-8"},
)
diff --git a/src/yunohost/tests/test_dns.py b/src/yunohost/tests/test_dns.py
new file mode 100644
index 000000000..a23ac7982
--- /dev/null
+++ b/src/yunohost/tests/test_dns.py
@@ -0,0 +1,85 @@
+import pytest
+
+from moulinette.utils.filesystem import read_toml
+
+from yunohost.domain import domain_add, domain_remove
+from yunohost.dns import (
+ DOMAIN_REGISTRAR_LIST_PATH,
+ _get_dns_zone_for_domain,
+ _get_registrar_config_section,
+ _build_dns_conf,
+)
+
+
+def setup_function(function):
+
+ clean()
+
+
+def teardown_function(function):
+
+ clean()
+
+
+def clean():
+ pass
+
+
+# DNS utils testing
+def test_get_dns_zone_from_domain_existing():
+ assert _get_dns_zone_for_domain("yunohost.org") == "yunohost.org"
+ assert _get_dns_zone_for_domain("donate.yunohost.org") == "yunohost.org"
+ assert _get_dns_zone_for_domain("fr.wikipedia.org") == "wikipedia.org"
+ assert _get_dns_zone_for_domain("www.fr.wikipedia.org") == "wikipedia.org"
+ assert (
+ _get_dns_zone_for_domain("non-existing-domain.yunohost.org") == "yunohost.org"
+ )
+ assert _get_dns_zone_for_domain("yolo.nohost.me") == "yolo.nohost.me"
+ assert _get_dns_zone_for_domain("foo.yolo.nohost.me") == "yolo.nohost.me"
+ assert _get_dns_zone_for_domain("bar.foo.yolo.nohost.me") == "yolo.nohost.me"
+
+ assert _get_dns_zone_for_domain("yolo.test") == "yolo.test"
+ assert _get_dns_zone_for_domain("foo.yolo.test") == "yolo.test"
+
+ assert _get_dns_zone_for_domain("yolo.tld") == "yolo.tld"
+ assert _get_dns_zone_for_domain("foo.yolo.tld") == "yolo.tld"
+
+
+# Domain registrar testing
+def test_registrar_list_integrity():
+ assert read_toml(DOMAIN_REGISTRAR_LIST_PATH)
+
+
+def test_magic_guess_registrar_weird_domain():
+ assert _get_registrar_config_section("yolo.tld")["registrar"]["value"] is None
+
+
+def test_magic_guess_registrar_ovh():
+ assert (
+ _get_registrar_config_section("yolo.yunohost.org")["registrar"]["value"]
+ == "ovh"
+ )
+
+
+def test_magic_guess_registrar_yunodyndns():
+ assert (
+ _get_registrar_config_section("yolo.nohost.me")["registrar"]["value"]
+ == "yunohost"
+ )
+
+
+@pytest.fixture
+def example_domain():
+ domain_add("example.tld")
+ yield "example.tld"
+ domain_remove("example.tld")
+
+
+def test_domain_dns_suggest(example_domain):
+
+ assert _build_dns_conf(example_domain)
+
+
+# def domain_dns_push(domain, dry_run):
+# import yunohost.dns
+# return yunohost.dns.domain_registrar_push(domain, dry_run)
diff --git a/src/yunohost/tests/test_domains.py b/src/yunohost/tests/test_domains.py
new file mode 100644
index 000000000..02d60ead4
--- /dev/null
+++ b/src/yunohost/tests/test_domains.py
@@ -0,0 +1,118 @@
+import pytest
+import os
+
+from moulinette.core import MoulinetteError
+
+from yunohost.utils.error import YunohostValidationError
+from yunohost.domain import (
+ DOMAIN_SETTINGS_DIR,
+ _get_maindomain,
+ domain_add,
+ domain_remove,
+ domain_list,
+ domain_main_domain,
+ domain_config_get,
+ domain_config_set,
+)
+
+TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"]
+
+
+def setup_function(function):
+
+ # Save domain list in variable to avoid multiple calls to domain_list()
+ domains = domain_list()["domains"]
+
+ # First domain is main domain
+ if not TEST_DOMAINS[0] in domains:
+ domain_add(TEST_DOMAINS[0])
+ else:
+ # Reset settings if any
+ os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{TEST_DOMAINS[0]}.yml")
+
+ if not _get_maindomain() == TEST_DOMAINS[0]:
+ domain_main_domain(TEST_DOMAINS[0])
+
+ # Clear other domains
+ for domain in domains:
+ if domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]:
+ # Clean domains not used for testing
+ domain_remove(domain)
+ elif domain in TEST_DOMAINS:
+ # Reset settings if any
+ os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{domain}.yml")
+
+ # Create classical second domain of not exist
+ if TEST_DOMAINS[1] not in domains:
+ domain_add(TEST_DOMAINS[1])
+
+ # Third domain is not created
+
+ clean()
+
+
+def teardown_function(function):
+
+ clean()
+
+
+def clean():
+ pass
+
+
+# Domains management testing
+def test_domain_add():
+ assert TEST_DOMAINS[2] not in domain_list()["domains"]
+ domain_add(TEST_DOMAINS[2])
+ assert TEST_DOMAINS[2] in domain_list()["domains"]
+
+
+def test_domain_add_existing_domain():
+ with pytest.raises(MoulinetteError):
+ assert TEST_DOMAINS[1] in domain_list()["domains"]
+ domain_add(TEST_DOMAINS[1])
+
+
+def test_domain_remove():
+ assert TEST_DOMAINS[1] in domain_list()["domains"]
+ domain_remove(TEST_DOMAINS[1])
+ assert TEST_DOMAINS[1] not in domain_list()["domains"]
+
+
+def test_main_domain():
+ current_main_domain = _get_maindomain()
+ assert domain_main_domain()["current_main_domain"] == current_main_domain
+
+
+def test_main_domain_change_unknown():
+ with pytest.raises(YunohostValidationError):
+ domain_main_domain(TEST_DOMAINS[2])
+
+
+def test_change_main_domain():
+ assert _get_maindomain() != TEST_DOMAINS[1]
+ domain_main_domain(TEST_DOMAINS[1])
+ assert _get_maindomain() == TEST_DOMAINS[1]
+
+
+# Domain settings testing
+def test_domain_config_get_default():
+ assert domain_config_get(TEST_DOMAINS[0], "feature.xmpp.xmpp") == 1
+ assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 0
+
+
+def test_domain_config_get_export():
+
+ assert domain_config_get(TEST_DOMAINS[0], export=True)["xmpp"] == 1
+ assert domain_config_get(TEST_DOMAINS[1], export=True)["xmpp"] == 0
+
+
+def test_domain_config_set():
+ assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 0
+ domain_config_set(TEST_DOMAINS[1], "feature.xmpp.xmpp", "yes")
+ assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 1
+
+
+def test_domain_configs_unknown():
+ with pytest.raises(YunohostValidationError):
+ domain_config_get(TEST_DOMAINS[2], "feature.xmpp.xmpp.xmpp")
diff --git a/src/yunohost/tests/test_ldapauth.py b/src/yunohost/tests/test_ldapauth.py
new file mode 100644
index 000000000..a95dea443
--- /dev/null
+++ b/src/yunohost/tests/test_ldapauth.py
@@ -0,0 +1,59 @@
+import pytest
+import os
+
+from yunohost.authenticators.ldap_admin import Authenticator as LDAPAuth
+from yunohost.tools import tools_adminpw
+
+from moulinette import m18n
+from moulinette.core import MoulinetteError
+
+
+def setup_function(function):
+
+ if os.system("systemctl is-active slapd") != 0:
+ os.system("systemctl start slapd && sleep 3")
+
+ tools_adminpw("yunohost", check_strength=False)
+
+
+def test_authenticate():
+ LDAPAuth().authenticate_credentials(credentials="yunohost")
+
+
+def test_authenticate_with_wrong_password():
+ with pytest.raises(MoulinetteError) as exception:
+ LDAPAuth().authenticate_credentials(credentials="bad_password_lul")
+
+ translation = m18n.n("invalid_password")
+ expected_msg = translation.format()
+ assert expected_msg in str(exception)
+
+
+def test_authenticate_server_down(mocker):
+ os.system("systemctl stop slapd && sleep 3")
+
+ # Now if slapd is down, moulinette tries to restart it
+ mocker.patch("os.system")
+ mocker.patch("time.sleep")
+ with pytest.raises(MoulinetteError) as exception:
+ LDAPAuth().authenticate_credentials(credentials="yunohost")
+
+ translation = m18n.n("ldap_server_down")
+ expected_msg = translation.format()
+ assert expected_msg in str(exception)
+
+
+def test_authenticate_change_password():
+
+ LDAPAuth().authenticate_credentials(credentials="yunohost")
+
+ tools_adminpw("plopette", check_strength=False)
+
+ with pytest.raises(MoulinetteError) as exception:
+ LDAPAuth().authenticate_credentials(credentials="yunohost")
+
+ translation = m18n.n("invalid_password")
+ expected_msg = translation.format()
+ assert expected_msg in str(exception)
+
+ LDAPAuth().authenticate_credentials(credentials="plopette")
diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py
index b33c2f213..00799d0fd 100644
--- a/src/yunohost/tests/test_permission.py
+++ b/src/yunohost/tests/test_permission.py
@@ -1049,7 +1049,7 @@ def test_permission_app_remove():
def test_permission_app_change_url():
app_install(
os.path.join(get_test_apps_dir(), "permissions_app_ynh"),
- args="domain=%s&domain_2=%s&path=%s&admin=%s"
+ args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s"
% (maindomain, other_domains[0], "/urlpermissionapp", "alice"),
force=True,
)
@@ -1072,7 +1072,7 @@ def test_permission_app_change_url():
def test_permission_protection_management_by_helper():
app_install(
os.path.join(get_test_apps_dir(), "permissions_app_ynh"),
- args="domain=%s&domain_2=%s&path=%s&admin=%s"
+ args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s"
% (maindomain, other_domains[0], "/urlpermissionapp", "alice"),
force=True,
)
@@ -1135,7 +1135,7 @@ def test_permission_legacy_app_propagation_on_ssowat():
app_install(
os.path.join(get_test_apps_dir(), "legacy_app_ynh"),
- args="domain=%s&domain_2=%s&path=%s"
+ args="domain=%s&domain_2=%s&path=%s&is_public=1"
% (maindomain, other_domains[0], "/legacy"),
force=True,
)
diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py
new file mode 100644
index 000000000..c21ff8c40
--- /dev/null
+++ b/src/yunohost/tests/test_questions.py
@@ -0,0 +1,2189 @@
+import sys
+import pytest
+import os
+
+from mock import patch
+from io import StringIO
+
+from moulinette import Moulinette
+
+from yunohost import domain, user
+from yunohost.utils.config import (
+ ask_questions_and_parse_answers,
+ PasswordQuestion,
+ DomainQuestion,
+ PathQuestion,
+ BooleanQuestion,
+ FileQuestion,
+ evaluate_simple_js_expression,
+)
+from yunohost.utils.error import YunohostError, YunohostValidationError
+
+
+"""
+Argument default format:
+{
+ "name": "the_name",
+ "type": "one_of_the_available_type", // "sting" is not specified
+ "ask": {
+ "en": "the question in english",
+ "fr": "the question in french"
+ },
+ "help": {
+ "en": "some help text in english",
+ "fr": "some help text in french"
+ },
+ "example": "an example value", // optional
+ "default", "some stuff", // optional, not available for all types
+ "optional": true // optional, will skip if not answered
+}
+
+User answers:
+{"name": "value", ...}
+"""
+
+
+def test_question_empty():
+ ask_questions_and_parse_answers([], {}) == []
+
+
+def test_question_string():
+ questions = [
+ {
+ "name": "some_string",
+ "type": "string",
+ }
+ ]
+ answers = {"some_string": "some_value"}
+
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_from_query_string():
+
+ questions = [
+ {
+ "name": "some_string",
+ "type": "string",
+ }
+ ]
+ answers = "foo=bar&some_string=some_value&lorem=ipsum"
+
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_default_type():
+ questions = [
+ {
+ "name": "some_string",
+ }
+ ]
+ answers = {"some_string": "some_value"}
+
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_no_input():
+ questions = [
+ {
+ "name": "some_string",
+ }
+ ]
+ answers = {}
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_string_input():
+ questions = [
+ {
+ "name": "some_string",
+ "ask": "some question",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_input_no_ask():
+ questions = [
+ {
+ "name": "some_string",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_no_input_optional():
+ questions = [
+ {
+ "name": "some_string",
+ "optional": True,
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == ""
+
+
+def test_question_string_optional_with_input():
+ questions = [
+ {
+ "name": "some_string",
+ "ask": "some question",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_optional_with_empty_input():
+ questions = [
+ {
+ "name": "some_string",
+ "ask": "some question",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value=""), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == ""
+
+
+def test_question_string_optional_with_input_without_ask():
+ questions = [
+ {
+ "name": "some_string",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_no_input_default():
+ questions = [
+ {
+ "name": "some_string",
+ "ask": "some question",
+ "default": "some_value",
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "some_value"
+
+
+def test_question_string_input_test_ask():
+ ask_text = "some question"
+ questions = [
+ {
+ "name": "some_string",
+ "ask": ask_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text,
+ is_password=False,
+ confirm=False,
+ prefill="",
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+def test_question_string_input_test_ask_with_default():
+ ask_text = "some question"
+ default_text = "some example"
+ questions = [
+ {
+ "name": "some_string",
+ "ask": ask_text,
+ "default": default_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text,
+ is_password=False,
+ confirm=False,
+ prefill=default_text,
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+@pytest.mark.skip # we should do something with this example
+def test_question_string_input_test_ask_with_example():
+ ask_text = "some question"
+ example_text = "some example"
+ questions = [
+ {
+ "name": "some_string",
+ "ask": ask_text,
+ "example": example_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert example_text in prompt.call_args[1]["message"]
+
+
+@pytest.mark.skip # we should do something with this help
+def test_question_string_input_test_ask_with_help():
+ ask_text = "some question"
+ help_text = "some_help"
+ questions = [
+ {
+ "name": "some_string",
+ "ask": ask_text,
+ "help": help_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert help_text in prompt.call_args[1]["message"]
+
+
+def test_question_string_with_choice():
+ questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}]
+ answers = {"some_string": "fr"}
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "fr"
+
+
+def test_question_string_with_choice_prompt():
+ questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}]
+ answers = {"some_string": "fr"}
+ with patch.object(Moulinette, "prompt", return_value="fr"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "fr"
+
+
+def test_question_string_with_choice_bad():
+ questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}]
+ answers = {"some_string": "bad"}
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_string_with_choice_ask():
+ ask_text = "some question"
+ choices = ["fr", "en", "es", "it", "ru"]
+ questions = [
+ {
+ "name": "some_string",
+ "ask": ask_text,
+ "choices": choices,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object(
+ os, "isatty", return_value=True
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+
+ for choice in choices:
+ assert choice in prompt.call_args[1]["message"]
+
+
+def test_question_string_with_choice_default():
+ questions = [
+ {
+ "name": "some_string",
+ "type": "string",
+ "choices": ["fr", "en"],
+ "default": "en",
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_string"
+ assert out.type == "string"
+ assert out.value == "en"
+
+
+def test_question_password():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ }
+ ]
+ answers = {"some_password": "some_value"}
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == "some_value"
+
+
+def test_question_password_no_input():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ }
+ ]
+ answers = {}
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_password_input():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": "some question",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == "some_value"
+
+
+def test_question_password_input_no_ask():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == "some_value"
+
+
+def test_question_password_no_input_optional():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == ""
+
+ questions = [
+ {"name": "some_password", "type": "password", "optional": True, "default": ""}
+ ]
+
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == ""
+
+
+def test_question_password_optional_with_input():
+ questions = [
+ {
+ "name": "some_password",
+ "ask": "some question",
+ "type": "password",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == "some_value"
+
+
+def test_question_password_optional_with_empty_input():
+ questions = [
+ {
+ "name": "some_password",
+ "ask": "some question",
+ "type": "password",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value=""), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == ""
+
+
+def test_question_password_optional_with_input_without_ask():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_password"
+ assert out.type == "password"
+ assert out.value == "some_value"
+
+
+def test_question_password_no_input_default():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": "some question",
+ "default": "some_value",
+ }
+ ]
+ answers = {}
+
+ # no default for password!
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+@pytest.mark.skip # this should raises
+def test_question_password_no_input_example():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": "some question",
+ "example": "some_value",
+ }
+ ]
+ answers = {"some_password": "some_value"}
+
+ # no example for password!
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_password_input_test_ask():
+ ask_text = "some question"
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": ask_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text,
+ is_password=True,
+ confirm=False,
+ prefill="",
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+@pytest.mark.skip # we should do something with this example
+def test_question_password_input_test_ask_with_example():
+ ask_text = "some question"
+ example_text = "some example"
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": ask_text,
+ "example": example_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert example_text in prompt.call_args[1]["message"]
+
+
+@pytest.mark.skip # we should do something with this help
+def test_question_password_input_test_ask_with_help():
+ ask_text = "some question"
+ help_text = "some_help"
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": ask_text,
+ "help": help_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert help_text in prompt.call_args[1]["message"]
+
+
+def test_question_password_bad_chars():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": "some question",
+ "example": "some_value",
+ }
+ ]
+
+ for i in PasswordQuestion.forbidden_chars:
+ with pytest.raises(YunohostError), patch.object(
+ os, "isatty", return_value=False
+ ):
+ ask_questions_and_parse_answers(questions, {"some_password": i * 8})
+
+
+def test_question_password_strong_enough():
+ questions = [
+ {
+ "name": "some_password",
+ "type": "password",
+ "ask": "some question",
+ "example": "some_value",
+ }
+ ]
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ # too short
+ ask_questions_and_parse_answers(questions, {"some_password": "a"})
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, {"some_password": "password"})
+
+
+def test_question_password_optional_strong_enough():
+ questions = [
+ {
+ "name": "some_password",
+ "ask": "some question",
+ "type": "password",
+ "optional": True,
+ }
+ ]
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ # too short
+ ask_questions_and_parse_answers(questions, {"some_password": "a"})
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, {"some_password": "password"})
+
+
+def test_question_path():
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ }
+ ]
+ answers = {"some_path": "/some_value"}
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == "/some_value"
+
+
+def test_question_path_no_input():
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ }
+ ]
+ answers = {}
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_path_input():
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ "ask": "some question",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == "/some_value"
+
+
+def test_question_path_input_no_ask():
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == "/some_value"
+
+
+def test_question_path_no_input_optional():
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ "optional": True,
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == ""
+
+
+def test_question_path_optional_with_input():
+ questions = [
+ {
+ "name": "some_path",
+ "ask": "some question",
+ "type": "path",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == "/some_value"
+
+
+def test_question_path_optional_with_empty_input():
+ questions = [
+ {
+ "name": "some_path",
+ "ask": "some question",
+ "type": "path",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value=""), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == ""
+
+
+def test_question_path_optional_with_input_without_ask():
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="/some_value"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == "/some_value"
+
+
+def test_question_path_no_input_default():
+ questions = [
+ {
+ "name": "some_path",
+ "ask": "some question",
+ "type": "path",
+ "default": "some_value",
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_path"
+ assert out.type == "path"
+ assert out.value == "/some_value"
+
+
+def test_question_path_input_test_ask():
+ ask_text = "some question"
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ "ask": ask_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text,
+ is_password=False,
+ confirm=False,
+ prefill="",
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+def test_question_path_input_test_ask_with_default():
+ ask_text = "some question"
+ default_text = "someexample"
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ "ask": ask_text,
+ "default": default_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text,
+ is_password=False,
+ confirm=False,
+ prefill=default_text,
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+@pytest.mark.skip # we should do something with this example
+def test_question_path_input_test_ask_with_example():
+ ask_text = "some question"
+ example_text = "some example"
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ "ask": ask_text,
+ "example": example_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert example_text in prompt.call_args[1]["message"]
+
+
+@pytest.mark.skip # we should do something with this help
+def test_question_path_input_test_ask_with_help():
+ ask_text = "some question"
+ help_text = "some_help"
+ questions = [
+ {
+ "name": "some_path",
+ "type": "path",
+ "ask": ask_text,
+ "help": help_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="some_value"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert help_text in prompt.call_args[1]["message"]
+
+
+def test_question_boolean():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ }
+ ]
+ answers = {"some_boolean": "y"}
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_boolean"
+ assert out.type == "boolean"
+ assert out.value == 1
+
+
+def test_question_boolean_all_yes():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ }
+ ]
+
+ for value in ["Y", "yes", "Yes", "YES", "1", 1, True, "True", "TRUE", "true"]:
+ out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0]
+ assert out.name == "some_boolean"
+ assert out.type == "boolean"
+ assert out.value == 1
+
+
+def test_question_boolean_all_no():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ }
+ ]
+
+ for value in ["n", "N", "no", "No", "No", "0", 0, False, "False", "FALSE", "false"]:
+ out = ask_questions_and_parse_answers(questions, {"some_boolean": value})[0]
+ assert out.name == "some_boolean"
+ assert out.type == "boolean"
+ assert out.value == 0
+
+
+# XXX apparently boolean are always False (0) by default, I'm not sure what to think about that
+def test_question_boolean_no_input():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ }
+ ]
+ answers = {}
+
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.value == 0
+
+
+def test_question_boolean_bad_input():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ }
+ ]
+ answers = {"some_boolean": "stuff"}
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_boolean_input():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ "ask": "some question",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="y"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+ assert out.value == 1
+
+ with patch.object(Moulinette, "prompt", return_value="n"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+ assert out.value == 0
+
+
+def test_question_boolean_input_no_ask():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="y"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+ assert out.value == 1
+
+
+def test_question_boolean_no_input_optional():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ "optional": True,
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+ assert out.value == 0
+
+
+def test_question_boolean_optional_with_input():
+ questions = [
+ {
+ "name": "some_boolean",
+ "ask": "some question",
+ "type": "boolean",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="y"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+ assert out.value == 1
+
+
+def test_question_boolean_optional_with_empty_input():
+ questions = [
+ {
+ "name": "some_boolean",
+ "ask": "some question",
+ "type": "boolean",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value=""), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.value == 0
+
+
+def test_question_boolean_optional_with_input_without_ask():
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="n"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.value == 0
+
+
+def test_question_boolean_no_input_default():
+ questions = [
+ {
+ "name": "some_boolean",
+ "ask": "some question",
+ "type": "boolean",
+ "default": 0,
+ }
+ ]
+ answers = {}
+
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.value == 0
+
+
+def test_question_boolean_bad_default():
+ questions = [
+ {
+ "name": "some_boolean",
+ "ask": "some question",
+ "type": "boolean",
+ "default": "bad default",
+ }
+ ]
+ answers = {}
+ with pytest.raises(YunohostError):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_boolean_input_test_ask():
+ ask_text = "some question"
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ "ask": ask_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value=0) as prompt, patch.object(
+ os, "isatty", return_value=True
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text + " [yes | no]",
+ is_password=False,
+ confirm=False,
+ prefill="no",
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+def test_question_boolean_input_test_ask_with_default():
+ ask_text = "some question"
+ default_text = 1
+ questions = [
+ {
+ "name": "some_boolean",
+ "type": "boolean",
+ "ask": ask_text,
+ "default": default_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value=1) as prompt, patch.object(
+ os, "isatty", return_value=True
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text + " [yes | no]",
+ is_password=False,
+ confirm=False,
+ prefill="yes",
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+def test_question_domain_empty():
+ questions = [
+ {
+ "name": "some_domain",
+ "type": "domain",
+ }
+ ]
+ main_domain = "my_main_domain.com"
+ answers = {}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value="my_main_domain.com"
+ ), patch.object(
+ domain, "domain_list", return_value={"domains": [main_domain]}
+ ), patch.object(
+ os, "isatty", return_value=False
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == main_domain
+
+
+def test_question_domain():
+ main_domain = "my_main_domain.com"
+ domains = [main_domain]
+ questions = [
+ {
+ "name": "some_domain",
+ "type": "domain",
+ }
+ ]
+
+ answers = {"some_domain": main_domain}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value=main_domain
+ ), patch.object(domain, "domain_list", return_value={"domains": domains}):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == main_domain
+
+
+def test_question_domain_two_domains():
+ main_domain = "my_main_domain.com"
+ other_domain = "some_other_domain.tld"
+ domains = [main_domain, other_domain]
+
+ questions = [
+ {
+ "name": "some_domain",
+ "type": "domain",
+ }
+ ]
+ answers = {"some_domain": other_domain}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value=main_domain
+ ), patch.object(domain, "domain_list", return_value={"domains": domains}):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == other_domain
+
+ answers = {"some_domain": main_domain}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value=main_domain
+ ), patch.object(domain, "domain_list", return_value={"domains": domains}):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == main_domain
+
+
+def test_question_domain_two_domains_wrong_answer():
+ main_domain = "my_main_domain.com"
+ other_domain = "some_other_domain.tld"
+ domains = [main_domain, other_domain]
+
+ questions = [
+ {
+ "name": "some_domain",
+ "type": "domain",
+ }
+ ]
+ answers = {"some_domain": "doesnt_exist.pouet"}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value=main_domain
+ ), patch.object(domain, "domain_list", return_value={"domains": domains}):
+ with pytest.raises(YunohostError), patch.object(
+ os, "isatty", return_value=False
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_domain_two_domains_default_no_ask():
+ main_domain = "my_main_domain.com"
+ other_domain = "some_other_domain.tld"
+ domains = [main_domain, other_domain]
+
+ questions = [
+ {
+ "name": "some_domain",
+ "type": "domain",
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value=main_domain
+ ), patch.object(
+ domain, "domain_list", return_value={"domains": domains}
+ ), patch.object(
+ os, "isatty", return_value=False
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == main_domain
+
+
+def test_question_domain_two_domains_default():
+ main_domain = "my_main_domain.com"
+ other_domain = "some_other_domain.tld"
+ domains = [main_domain, other_domain]
+
+ questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}]
+ answers = {}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value=main_domain
+ ), patch.object(
+ domain, "domain_list", return_value={"domains": domains}
+ ), patch.object(
+ os, "isatty", return_value=False
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == main_domain
+
+
+def test_question_domain_two_domains_default_input():
+ main_domain = "my_main_domain.com"
+ other_domain = "some_other_domain.tld"
+ domains = [main_domain, other_domain]
+
+ questions = [{"name": "some_domain", "type": "domain", "ask": "choose a domain"}]
+ answers = {}
+
+ with patch.object(
+ domain, "_get_maindomain", return_value=main_domain
+ ), patch.object(
+ domain, "domain_list", return_value={"domains": domains}
+ ), patch.object(
+ os, "isatty", return_value=True
+ ):
+ with patch.object(Moulinette, "prompt", return_value=main_domain):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == main_domain
+
+ with patch.object(Moulinette, "prompt", return_value=other_domain):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_domain"
+ assert out.type == "domain"
+ assert out.value == other_domain
+
+
+def test_question_user_empty():
+ users = {
+ "some_user": {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "p@ynh.local",
+ "fullname": "the first name the last name",
+ }
+ }
+
+ questions = [
+ {
+ "name": "some_user",
+ "type": "user",
+ }
+ ]
+ answers = {}
+
+ with patch.object(user, "user_list", return_value={"users": users}):
+ with pytest.raises(YunohostError), patch.object(
+ os, "isatty", return_value=False
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_user():
+ username = "some_user"
+ users = {
+ username: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "p@ynh.local",
+ "fullname": "the first name the last name",
+ }
+ }
+
+ questions = [
+ {
+ "name": "some_user",
+ "type": "user",
+ }
+ ]
+ answers = {"some_user": username}
+
+ with patch.object(user, "user_list", return_value={"users": users}), patch.object(
+ user, "user_info", return_value={}
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_user"
+ assert out.type == "user"
+ assert out.value == username
+
+
+def test_question_user_two_users():
+ username = "some_user"
+ other_user = "some_other_user"
+ users = {
+ username: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "p@ynh.local",
+ "fullname": "the first name the last name",
+ },
+ other_user: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "z@ynh.local",
+ "fullname": "john doe",
+ },
+ }
+
+ questions = [
+ {
+ "name": "some_user",
+ "type": "user",
+ }
+ ]
+ answers = {"some_user": other_user}
+
+ with patch.object(user, "user_list", return_value={"users": users}), patch.object(
+ user, "user_info", return_value={}
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_user"
+ assert out.type == "user"
+ assert out.value == other_user
+
+ answers = {"some_user": username}
+
+ with patch.object(user, "user_list", return_value={"users": users}), patch.object(
+ user, "user_info", return_value={}
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_user"
+ assert out.type == "user"
+ assert out.value == username
+
+
+def test_question_user_two_users_wrong_answer():
+ username = "my_username.com"
+ other_user = "some_other_user"
+ users = {
+ username: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "p@ynh.local",
+ "fullname": "the first name the last name",
+ },
+ other_user: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "z@ynh.local",
+ "fullname": "john doe",
+ },
+ }
+
+ questions = [
+ {
+ "name": "some_user",
+ "type": "user",
+ }
+ ]
+ answers = {"some_user": "doesnt_exist.pouet"}
+
+ with patch.object(user, "user_list", return_value={"users": users}):
+ with pytest.raises(YunohostError), patch.object(
+ os, "isatty", return_value=False
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_user_two_users_no_default():
+ username = "my_username.com"
+ other_user = "some_other_user.tld"
+ users = {
+ username: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "p@ynh.local",
+ "fullname": "the first name the last name",
+ },
+ other_user: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "z@ynh.local",
+ "fullname": "john doe",
+ },
+ }
+
+ questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}]
+ answers = {}
+
+ with patch.object(user, "user_list", return_value={"users": users}):
+ with pytest.raises(YunohostError), patch.object(
+ os, "isatty", return_value=False
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_user_two_users_default_input():
+ username = "my_username.com"
+ other_user = "some_other_user.tld"
+ users = {
+ username: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "p@ynh.local",
+ "fullname": "the first name the last name",
+ },
+ other_user: {
+ "ssh_allowed": False,
+ "username": "some_user",
+ "mailbox-quota": "0",
+ "mail": "z@ynh.local",
+ "fullname": "john doe",
+ },
+ }
+
+ questions = [{"name": "some_user", "type": "user", "ask": "choose a user"}]
+ answers = {}
+
+ with patch.object(user, "user_list", return_value={"users": users}), patch.object(
+ os, "isatty", return_value=True
+ ):
+ with patch.object(user, "user_info", return_value={}):
+
+ with patch.object(Moulinette, "prompt", return_value=username):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_user"
+ assert out.type == "user"
+ assert out.value == username
+
+ with patch.object(Moulinette, "prompt", return_value=other_user):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_user"
+ assert out.type == "user"
+ assert out.value == other_user
+
+
+def test_question_number():
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ }
+ ]
+ answers = {"some_number": 1337}
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 1337
+
+
+def test_question_number_no_input():
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ }
+ ]
+ answers = {}
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_number_bad_input():
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ }
+ ]
+ answers = {"some_number": "stuff"}
+
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+ answers = {"some_number": 1.5}
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_number_input():
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ "ask": "some question",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="1337"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 1337
+
+ with patch.object(Moulinette, "prompt", return_value=1337), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 1337
+
+ with patch.object(Moulinette, "prompt", return_value="0"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 0
+
+
+def test_question_number_input_no_ask():
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="1337"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 1337
+
+
+def test_question_number_no_input_optional():
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ "optional": True,
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value is None
+
+
+def test_question_number_optional_with_input():
+ questions = [
+ {
+ "name": "some_number",
+ "ask": "some question",
+ "type": "number",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="1337"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 1337
+
+
+def test_question_number_optional_with_input_without_ask():
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ "optional": True,
+ }
+ ]
+ answers = {}
+
+ with patch.object(Moulinette, "prompt", return_value="0"), patch.object(
+ os, "isatty", return_value=True
+ ):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 0
+
+
+def test_question_number_no_input_default():
+ questions = [
+ {
+ "name": "some_number",
+ "ask": "some question",
+ "type": "number",
+ "default": 1337,
+ }
+ ]
+ answers = {}
+ with patch.object(os, "isatty", return_value=False):
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_number"
+ assert out.type == "number"
+ assert out.value == 1337
+
+
+def test_question_number_bad_default():
+ questions = [
+ {
+ "name": "some_number",
+ "ask": "some question",
+ "type": "number",
+ "default": "bad default",
+ }
+ ]
+ answers = {}
+ with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False):
+ ask_questions_and_parse_answers(questions, answers)
+
+
+def test_question_number_input_test_ask():
+ ask_text = "some question"
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ "ask": ask_text,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="1111"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text,
+ is_password=False,
+ confirm=False,
+ prefill="",
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+def test_question_number_input_test_ask_with_default():
+ ask_text = "some question"
+ default_value = 1337
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ "ask": ask_text,
+ "default": default_value,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="1111"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ prompt.assert_called_with(
+ message=ask_text,
+ is_password=False,
+ confirm=False,
+ prefill=str(default_value),
+ is_multiline=False,
+ autocomplete=[],
+ help=None,
+ )
+
+
+@pytest.mark.skip # we should do something with this example
+def test_question_number_input_test_ask_with_example():
+ ask_text = "some question"
+ example_value = 1337
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ "ask": ask_text,
+ "example": example_value,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="1111"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert example_value in prompt.call_args[1]["message"]
+
+
+@pytest.mark.skip # we should do something with this help
+def test_question_number_input_test_ask_with_help():
+ ask_text = "some question"
+ help_value = 1337
+ questions = [
+ {
+ "name": "some_number",
+ "type": "number",
+ "ask": ask_text,
+ "help": help_value,
+ }
+ ]
+ answers = {}
+
+ with patch.object(
+ Moulinette, "prompt", return_value="1111"
+ ) as prompt, patch.object(os, "isatty", return_value=True):
+ ask_questions_and_parse_answers(questions, answers)
+ assert ask_text in prompt.call_args[1]["message"]
+ assert help_value in prompt.call_args[1]["message"]
+
+
+def test_question_display_text():
+ questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}]
+ answers = {}
+
+ with patch.object(sys, "stdout", new_callable=StringIO) as stdout, patch.object(
+ os, "isatty", return_value=True
+ ):
+ ask_questions_and_parse_answers(questions, answers)
+ assert "foobar" in stdout.getvalue()
+
+
+def test_question_file_from_cli():
+
+ FileQuestion.clean_upload_dirs()
+
+ filename = "/tmp/ynh_test_question_file"
+ os.system(f"rm -f {filename}")
+ os.system(f"echo helloworld > {filename}")
+
+ questions = [
+ {
+ "name": "some_file",
+ "type": "file",
+ }
+ ]
+ answers = {"some_file": filename}
+
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+
+ assert out.name == "some_file"
+ assert out.type == "file"
+
+ # The file is supposed to be copied somewhere else
+ assert out.value != filename
+ assert out.value.startswith("/tmp/")
+ assert os.path.exists(out.value)
+ assert "helloworld" in open(out.value).read().strip()
+
+ FileQuestion.clean_upload_dirs()
+
+ assert not os.path.exists(out.value)
+
+
+def test_question_file_from_api():
+
+ FileQuestion.clean_upload_dirs()
+
+ from base64 import b64encode
+
+ b64content = b64encode("helloworld".encode())
+ questions = [
+ {
+ "name": "some_file",
+ "type": "file",
+ }
+ ]
+ answers = {"some_file": b64content}
+
+ interface_type_bkp = Moulinette.interface.type
+ try:
+ Moulinette.interface.type = "api"
+ out = ask_questions_and_parse_answers(questions, answers)[0]
+ finally:
+ Moulinette.interface.type = interface_type_bkp
+
+ assert out.name == "some_file"
+ assert out.type == "file"
+
+ assert out.value.startswith("/tmp/")
+ assert os.path.exists(out.value)
+ assert "helloworld" in open(out.value).read().strip()
+
+ FileQuestion.clean_upload_dirs()
+
+ assert not os.path.exists(out.value)
+
+
+def test_normalize_boolean_nominal():
+
+ assert BooleanQuestion.normalize("yes") == 1
+ assert BooleanQuestion.normalize("Yes") == 1
+ assert BooleanQuestion.normalize(" yes ") == 1
+ assert BooleanQuestion.normalize("y") == 1
+ assert BooleanQuestion.normalize("true") == 1
+ assert BooleanQuestion.normalize("True") == 1
+ assert BooleanQuestion.normalize("on") == 1
+ assert BooleanQuestion.normalize("1") == 1
+ assert BooleanQuestion.normalize(1) == 1
+
+ assert BooleanQuestion.normalize("no") == 0
+ assert BooleanQuestion.normalize("No") == 0
+ assert BooleanQuestion.normalize(" no ") == 0
+ assert BooleanQuestion.normalize("n") == 0
+ assert BooleanQuestion.normalize("false") == 0
+ assert BooleanQuestion.normalize("False") == 0
+ assert BooleanQuestion.normalize("off") == 0
+ assert BooleanQuestion.normalize("0") == 0
+ assert BooleanQuestion.normalize(0) == 0
+
+ assert BooleanQuestion.normalize("") is None
+ assert BooleanQuestion.normalize(" ") is None
+ assert BooleanQuestion.normalize(" none ") is None
+ assert BooleanQuestion.normalize("None") is None
+ assert BooleanQuestion.normalize("noNe") is None
+ assert BooleanQuestion.normalize(None) is None
+
+
+def test_normalize_boolean_humanize():
+
+ assert BooleanQuestion.humanize("yes") == "yes"
+ assert BooleanQuestion.humanize("true") == "yes"
+ assert BooleanQuestion.humanize("on") == "yes"
+
+ assert BooleanQuestion.humanize("no") == "no"
+ assert BooleanQuestion.humanize("false") == "no"
+ assert BooleanQuestion.humanize("off") == "no"
+
+
+def test_normalize_boolean_invalid():
+
+ with pytest.raises(YunohostValidationError):
+ BooleanQuestion.normalize("yesno")
+ with pytest.raises(YunohostValidationError):
+ BooleanQuestion.normalize("foobar")
+ with pytest.raises(YunohostValidationError):
+ BooleanQuestion.normalize("enabled")
+
+
+def test_normalize_boolean_special_yesno():
+
+ customyesno = {"yes": "enabled", "no": "disabled"}
+
+ assert BooleanQuestion.normalize("yes", customyesno) == "enabled"
+ assert BooleanQuestion.normalize("true", customyesno) == "enabled"
+ assert BooleanQuestion.normalize("enabled", customyesno) == "enabled"
+ assert BooleanQuestion.humanize("yes", customyesno) == "yes"
+ assert BooleanQuestion.humanize("true", customyesno) == "yes"
+ assert BooleanQuestion.humanize("enabled", customyesno) == "yes"
+
+ assert BooleanQuestion.normalize("no", customyesno) == "disabled"
+ assert BooleanQuestion.normalize("false", customyesno) == "disabled"
+ assert BooleanQuestion.normalize("disabled", customyesno) == "disabled"
+ assert BooleanQuestion.humanize("no", customyesno) == "no"
+ assert BooleanQuestion.humanize("false", customyesno) == "no"
+ assert BooleanQuestion.humanize("disabled", customyesno) == "no"
+
+
+def test_normalize_domain():
+
+ assert DomainQuestion.normalize("https://yolo.swag/") == "yolo.swag"
+ assert DomainQuestion.normalize("http://yolo.swag") == "yolo.swag"
+ assert DomainQuestion.normalize("yolo.swag/") == "yolo.swag"
+
+
+def test_normalize_path():
+
+ assert PathQuestion.normalize("") == "/"
+ assert PathQuestion.normalize("") == "/"
+ assert PathQuestion.normalize("macnuggets") == "/macnuggets"
+ assert PathQuestion.normalize("/macnuggets") == "/macnuggets"
+ assert PathQuestion.normalize(" /macnuggets ") == "/macnuggets"
+ assert PathQuestion.normalize("/macnuggets") == "/macnuggets"
+ assert PathQuestion.normalize("mac/nuggets") == "/mac/nuggets"
+ assert PathQuestion.normalize("/macnuggets/") == "/macnuggets"
+ assert PathQuestion.normalize("macnuggets/") == "/macnuggets"
+ assert PathQuestion.normalize("////macnuggets///") == "/macnuggets"
+
+
+def test_simple_evaluate():
+ context = {
+ "a1": 1,
+ "b2": 2,
+ "c10": 10,
+ "foo": "bar",
+ "comp": "1>2",
+ "empty": "",
+ "lorem": "Lorem ipsum dolor et si qua met!",
+ "warning": "Warning! This sentence will fail!",
+ "quote": "Je s'apelle Groot",
+ "and_": "&&",
+ "object": {"a": "Security risk"},
+ }
+ supported = {
+ "42": 42,
+ "9.5": 9.5,
+ "'bopbidibopbopbop'": "bopbidibopbopbop",
+ "true": True,
+ "false": False,
+ "null": None,
+ # Math
+ "1 * (2 + 3 * (4 - 3))": 5,
+ "1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3": True,
+ "(9 - 2) * 3 - 10": 11,
+ "12 - 2 * -2 + (3 - 4) * 3.1": 12.9,
+ "9 / 12 + 12 * 3 - 5": 31.75,
+ "9 / 12 + 12 * (3 - 5)": -23.25,
+ "12 > 13.1": False,
+ "12 < 14": True,
+ "12 <= 14": True,
+ "12 >= 14": False,
+ "12 == 14": False,
+ "12 % 5 > 3": False,
+ "12 != 14": True,
+ "9 - 1 > 10 && 3 * 5 > 10": False,
+ "9 - 1 > 10 || 3 * 5 > 10": True,
+ "a1 > 0 || a1 < -12": True,
+ "a1 > 0 && a1 < -12": False,
+ "a1 + 1 > 0 && -a1 > -12": True,
+ "-(a1 + 1) < 0 || -(a1 + 2) > -12": True,
+ "-a1 * 2": -2,
+ "(9 - 2) * 3 - c10": 11,
+ "(9 - b2) * 3 - c10": 11,
+ "c10 > b2": True,
+ # String
+ "foo == 'bar'": True,
+ "foo != 'bar'": False,
+ 'foo == "bar" && 1 > 0': True,
+ "!!foo": True,
+ "!foo": False,
+ "foo": "bar",
+ '!(foo > "baa") || 1 > 2': False,
+ '!(foo > "baa") || 1 < 2': True,
+ 'empty == ""': True,
+ '1 == "1"': True,
+ '1.0 == "1"': True,
+ '1 == "aaa"': False,
+ "'I am ' + b2 + ' years'": "I am 2 years",
+ "quote == 'Je s\\'apelle Groot'": True,
+ "lorem == 'Lorem ipsum dolor et si qua met!'": True,
+ "and_ == '&&'": True,
+ "warning == 'Warning! This sentence will fail!'": True,
+ # Match
+ "match(lorem, '^Lorem [ia]psumE?')": bool,
+ "match(foo, '^Lorem [ia]psumE?')": None,
+ "match(lorem, '^Lorem [ia]psumE?') && 1 == 1": bool,
+ # No code
+ "": False,
+ " ": False,
+ }
+ trigger_errors = {
+ "object.a": YunohostError, # Keep unsupported, for security reasons
+ "a1 ** b2": YunohostError, # Keep unsupported, for security reasons
+ "().__class__.__bases__[0].__subclasses__()": YunohostError, # Very dangerous code
+ "a1 > 11 ? 1 : 0": SyntaxError,
+ "c10 > b2 == false": YunohostError, # JS and Python doesn't do the same thing for this situation
+ "c10 > b2 == true": YunohostError,
+ }
+
+ for expression, result in supported.items():
+ if result == bool:
+ assert bool(evaluate_simple_js_expression(expression, context)), expression
+ else:
+ assert (
+ evaluate_simple_js_expression(expression, context) == result
+ ), expression
+
+ for expression, error in trigger_errors.items():
+ with pytest.raises(error):
+ evaluate_simple_js_expression(expression, context)
diff --git a/src/yunohost/tests/test_service.py b/src/yunohost/tests/test_service.py
index 1f82dc8fd..88013a3fe 100644
--- a/src/yunohost/tests/test_service.py
+++ b/src/yunohost/tests/test_service.py
@@ -9,6 +9,7 @@ from yunohost.service import (
service_add,
service_remove,
service_log,
+ service_reload_or_restart,
)
@@ -38,6 +39,10 @@ def clean():
_save_services(services)
+ if os.path.exists("/etc/nginx/conf.d/broken.conf"):
+ os.remove("/etc/nginx/conf.d/broken.conf")
+ os.system("systemctl reload-or-restart nginx")
+
def test_service_status_all():
@@ -118,3 +123,20 @@ def test_service_update_to_remove_properties():
assert _get_services()["dummyservice"].get("test_status") == "false"
service_add("dummyservice", description="dummy", test_status="")
assert not _get_services()["dummyservice"].get("test_status")
+
+
+def test_service_conf_broken():
+
+ os.system("echo pwet > /etc/nginx/conf.d/broken.conf")
+
+ status = service_status("nginx")
+ assert status["status"] == "running"
+ assert status["configuration"] == "broken"
+ assert "broken.conf" in status["configuration-details"][0]
+
+ # Service reload-or-restart should check that the conf ain't valid
+ # before reload-or-restart, hence the service should still be running
+ service_reload_or_restart("nginx")
+ assert status["status"] == "running"
+
+ os.remove("/etc/nginx/conf.d/broken.conf")
diff --git a/src/yunohost/tests/test_settings.py b/src/yunohost/tests/test_settings.py
index b402a9ef5..1a9063e56 100644
--- a/src/yunohost/tests/test_settings.py
+++ b/src/yunohost/tests/test_settings.py
@@ -1,9 +1,12 @@
import os
import json
+import glob
import pytest
from yunohost.utils.error import YunohostError
+import yunohost.settings as settings
+
from yunohost.settings import (
settings_get,
settings_list,
@@ -28,6 +31,15 @@ def setup_function(function):
def teardown_function(function):
os.system("mv /etc/yunohost/settings.json.saved /etc/yunohost/settings.json")
+ for filename in glob.glob("/etc/yunohost/settings-*.json"):
+ os.remove(filename)
+
+
+def monkey_get_setting_description(key):
+ return "Dummy %s setting" % key.split(".")[-1]
+
+
+settings._get_setting_description = monkey_get_setting_description
def test_settings_get_bool():
diff --git a/src/yunohost/tests/test_user-group.py b/src/yunohost/tests/test_user-group.py
index 251029796..60e748108 100644
--- a/src/yunohost/tests/test_user-group.py
+++ b/src/yunohost/tests/test_user-group.py
@@ -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"):
diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py
index e1ebe1307..e89081abd 100644
--- a/src/yunohost/tools.py
+++ b/src/yunohost/tools.py
@@ -25,21 +25,24 @@
"""
import re
import os
-import yaml
import subprocess
-import pwd
+import time
from importlib import import_module
+from packaging import version
+from typing import List
-from moulinette import msignals, m18n
+from moulinette import Moulinette, m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output, call_async_output
-from moulinette.utils.filesystem import read_yaml, write_to_yaml
+from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm
from yunohost.app import (
- _update_apps_catalog,
app_info,
app_upgrade,
+)
+from yunohost.app_catalog import (
_initialize_apps_catalog_system,
+ _update_apps_catalog,
)
from yunohost.domain import domain_add
from yunohost.dyndns import _dyndns_available, _dyndns_provides
@@ -51,7 +54,7 @@ from yunohost.utils.packages import (
_list_upgradable_apt_packages,
ynh_packages_version,
)
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.log import is_unit_operation, OperationLogger
# FIXME this is a duplicate from apps.py
@@ -65,79 +68,6 @@ def tools_versions():
return ynh_packages_version()
-def tools_ldapinit():
- """
- YunoHost LDAP initialization
- """
-
- with open("/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml") as f:
- ldap_map = yaml.load(f)
-
- from yunohost.utils.ldap import _get_ldap_interface
-
- ldap = _get_ldap_interface()
-
- for rdn, attr_dict in ldap_map["parents"].items():
- try:
- ldap.add(rdn, attr_dict)
- except Exception as e:
- logger.warn(
- "Error when trying to inject '%s' -> '%s' into ldap: %s"
- % (rdn, attr_dict, e)
- )
-
- for rdn, attr_dict in ldap_map["children"].items():
- try:
- ldap.add(rdn, attr_dict)
- except Exception as e:
- logger.warn(
- "Error when trying to inject '%s' -> '%s' into ldap: %s"
- % (rdn, attr_dict, e)
- )
-
- for rdn, attr_dict in ldap_map["depends_children"].items():
- try:
- ldap.add(rdn, attr_dict)
- except Exception as e:
- logger.warn(
- "Error when trying to inject '%s' -> '%s' into ldap: %s"
- % (rdn, attr_dict, e)
- )
-
- admin_dict = {
- "cn": ["admin"],
- "uid": ["admin"],
- "description": ["LDAP Administrator"],
- "gidNumber": ["1007"],
- "uidNumber": ["1007"],
- "homeDirectory": ["/home/admin"],
- "loginShell": ["/bin/bash"],
- "objectClass": ["organizationalRole", "posixAccount", "simpleSecurityObject"],
- "userPassword": ["yunohost"],
- }
-
- ldap.update("cn=admin", admin_dict)
-
- # Force nscd to refresh cache to take admin creation into account
- subprocess.call(["nscd", "-i", "passwd"])
-
- # Check admin actually exists now
- try:
- pwd.getpwnam("admin")
- except KeyError:
- logger.error(m18n.n("ldap_init_failed_to_create_admin"))
- raise YunohostError("installation_failed")
-
- try:
- # Attempt to create user home folder
- subprocess.check_call(["mkhomedir_helper", "admin"])
- except subprocess.CalledProcessError:
- if not os.path.isdir("/home/{0}".format("admin")):
- logger.warning(m18n.n("user_home_creation_failed"), exc_info=1)
-
- logger.success(m18n.n("ldap_initialized"))
-
-
def tools_adminpw(new_password, check_strength=True):
"""
Change admin password
@@ -156,7 +86,7 @@ def tools_adminpw(new_password, check_strength=True):
# UNIX seems to not like password longer than 127 chars ...
# e.g. SSH login gets broken (or even 'su admin' when entering the password)
if len(new_password) >= 127:
- raise YunohostError("admin_password_too_long")
+ raise YunohostValidationError("admin_password_too_long")
new_hash = _hash_user_password(new_password)
@@ -167,12 +97,10 @@ def tools_adminpw(new_password, check_strength=True):
try:
ldap.update(
"cn=admin",
- {
- "userPassword": [new_hash],
- },
+ {"userPassword": [new_hash]},
)
- except Exception:
- logger.error("unable to change admin password")
+ except Exception as e:
+ logger.error("unable to change admin password : %s" % e)
raise YunohostError("admin_password_change_failed")
else:
# Write as root password
@@ -285,10 +213,10 @@ def tools_postinstall(
# Do some checks at first
if os.path.isfile("/etc/yunohost/installed"):
- raise YunohostError("yunohost_already_installed")
+ raise YunohostValidationError("yunohost_already_installed")
if os.path.isdir("/etc/yunohost/apps") and os.listdir("/etc/yunohost/apps") != []:
- raise YunohostError(
+ raise YunohostValidationError(
"It looks like you're trying to re-postinstall a system that was already working previously ... If you recently had some bug or issues with your installation, please first discuss with the team on how to fix the situation instead of savagely re-running the postinstall ...",
raw_msg=True,
)
@@ -301,7 +229,7 @@ def tools_postinstall(
)
GB = 1024 ** 3
if not force_diskspace and main_space < 10 * GB:
- raise YunohostError("postinstall_low_rootfsspace")
+ raise YunohostValidationError("postinstall_low_rootfsspace")
# Check password
if not force_password:
@@ -331,14 +259,14 @@ def tools_postinstall(
dyndns = True
# If not, abort the postinstall
else:
- raise YunohostError("dyndns_unavailable", domain=domain)
+ raise YunohostValidationError("dyndns_unavailable", domain=domain)
else:
dyndns = False
else:
dyndns = False
if os.system("iptables -V >/dev/null 2>/dev/null") != 0:
- raise YunohostError(
+ raise YunohostValidationError(
"iptables/nftables does not seems to be working on your setup. You may be in a container or your kernel does have the proper modules loaded. Sometimes, rebooting the machine may solve the issue.",
raw_msg=True,
)
@@ -350,7 +278,7 @@ def tools_postinstall(
domain_add(domain, dyndns)
domain_main_domain(domain)
- # Change LDAP admin password
+ # Update LDAP admin and create home dir
tools_adminpw(password, check_strength=not force_password)
# Enable UPnP silently and reload firewall
@@ -404,22 +332,34 @@ def tools_regen_conf(
return regen_conf(names, with_diff, force, dry_run, list_pending)
-def tools_update(apps=False, system=False):
+def tools_update(target=None, apps=False, system=False):
"""
Update apps & system package cache
-
- Keyword arguments:
- system -- Fetch available system packages upgrades (equivalent to apt update)
- apps -- Fetch the application list to check which apps can be upgraded
"""
- # If neither --apps nor --system specified, do both
- if not apps and not system:
- apps = True
- system = True
+ # Legacy options (--system, --apps)
+ if apps or system:
+ logger.warning(
+ "Using 'yunohost tools update' with --apps / --system is deprecated, just write 'yunohost tools update apps system' (no -- prefix anymore)"
+ )
+ if apps and system:
+ target = "all"
+ elif apps:
+ target = "apps"
+ else:
+ target = "system"
+
+ elif not target:
+ target = "all"
+
+ if target not in ["system", "apps", "all"]:
+ raise YunohostError(
+ "Unknown target %s, should be 'system', 'apps' or 'all'" % target,
+ raw_msg=True,
+ )
upgradable_system_packages = []
- if system:
+ if target in ["system", "all"]:
# Update APT cache
# LC_ALL=C is here to make sure the results are in english
@@ -467,7 +407,7 @@ def tools_update(apps=False, system=False):
logger.debug(m18n.n("done"))
upgradable_apps = []
- if apps:
+ if target in ["apps", "all"]:
try:
_update_apps_catalog()
except YunohostError as e:
@@ -518,7 +458,7 @@ def _list_upgradable_apps():
@is_unit_operation()
def tools_upgrade(
- operation_logger, apps=None, system=False, allow_yunohost_upgrade=True
+ operation_logger, target=None, apps=False, system=False, allow_yunohost_upgrade=True
):
"""
Update apps & package cache, then display changelog
@@ -530,39 +470,51 @@ def tools_upgrade(
from yunohost.utils import packages
if packages.dpkg_is_broken():
- raise YunohostError("dpkg_is_broken")
+ raise YunohostValidationError("dpkg_is_broken")
# Check for obvious conflict with other dpkg/apt commands already running in parallel
if not packages.dpkg_lock_available():
- raise YunohostError("dpkg_lock_not_available")
+ raise YunohostValidationError("dpkg_lock_not_available")
- if system is not False and apps is not None:
- raise YunohostError("tools_upgrade_cant_both")
+ # Legacy options management (--system, --apps)
+ if target is None:
- if system is False and apps is None:
- raise YunohostError("tools_upgrade_at_least_one")
+ logger.warning(
+ "Using 'yunohost tools upgrade' with --apps / --system is deprecated, just write 'yunohost tools upgrade apps' or 'system' (no -- prefix anymore)"
+ )
+
+ if (system, apps) == (True, True):
+ raise YunohostValidationError("tools_upgrade_cant_both")
+
+ if (system, apps) == (False, False):
+ raise YunohostValidationError("tools_upgrade_at_least_one")
+
+ target = "apps" if apps else "system"
+
+ if target not in ["apps", "system"]:
+ raise Exception(
+ "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target"
+ )
#
# Apps
# This is basically just an alias to yunohost app upgrade ...
#
- if apps is not None:
+ if target == "apps":
# Make sure there's actually something to upgrade
upgradable_apps = [app["id"] for app in _list_upgradable_apps()]
- if not upgradable_apps or (
- len(apps) and all(app not in upgradable_apps for app in apps)
- ):
+ if not upgradable_apps:
logger.info(m18n.n("apps_already_up_to_date"))
return
# Actually start the upgrades
try:
- app_upgrade(app=apps)
+ app_upgrade(app=upgradable_apps)
except Exception as e:
logger.warning("unable to upgrade apps: %s" % str(e))
logger.error(m18n.n("app_upgrade_some_app_failed"))
@@ -573,7 +525,7 @@ def tools_upgrade(
# System
#
- if system is True:
+ if target == "system":
# Check that there's indeed some packages to upgrade
upgradables = list(_list_upgradable_apt_packages())
@@ -743,7 +695,7 @@ def tools_shutdown(operation_logger, force=False):
if not shutdown:
try:
# Ask confirmation for server shutdown
- i = msignals.prompt(m18n.n("server_shutdown_confirm", answers="y/N"))
+ i = Moulinette.prompt(m18n.n("server_shutdown_confirm", answers="y/N"))
except NotImplemented:
pass
else:
@@ -762,7 +714,7 @@ def tools_reboot(operation_logger, force=False):
if not reboot:
try:
# Ask confirmation for restoring
- i = msignals.prompt(m18n.n("server_reboot_confirm", answers="y/N"))
+ i = Moulinette.prompt(m18n.n("server_reboot_confirm", answers="y/N"))
except NotImplemented:
pass
else:
@@ -825,7 +777,7 @@ def tools_migrations_list(pending=False, done=False):
# Check for option conflict
if pending and done:
- raise YunohostError("migrations_list_conflict_pending_done")
+ raise YunohostValidationError("migrations_list_conflict_pending_done")
# Get all migrations
migrations = _get_migrations_list()
@@ -875,17 +827,17 @@ def tools_migrations_run(
if m.id == target or m.name == target or m.id.split("_")[0] == target:
return m
- raise YunohostError("migrations_no_such_migration", id=target)
+ raise YunohostValidationError("migrations_no_such_migration", id=target)
# auto, skip and force are exclusive options
if auto + skip + force_rerun > 1:
- raise YunohostError("migrations_exclusive_options")
+ raise YunohostValidationError("migrations_exclusive_options")
# If no target specified
if not targets:
# skip, revert or force require explicit targets
if skip or force_rerun:
- raise YunohostError("migrations_must_provide_explicit_targets")
+ raise YunohostValidationError("migrations_must_provide_explicit_targets")
# Otherwise, targets are all pending migrations
targets = [m for m in all_migrations if m.state == "pending"]
@@ -897,11 +849,15 @@ def tools_migrations_run(
pending = [t.id for t in targets if t.state == "pending"]
if skip and done:
- raise YunohostError("migrations_not_pending_cant_skip", ids=", ".join(done))
+ raise YunohostValidationError(
+ "migrations_not_pending_cant_skip", ids=", ".join(done)
+ )
if force_rerun and pending:
- raise YunohostError("migrations_pending_cant_rerun", ids=", ".join(pending))
+ raise YunohostValidationError(
+ "migrations_pending_cant_rerun", ids=", ".join(pending)
+ )
if not (skip or force_rerun) and done:
- raise YunohostError("migrations_already_ran", ids=", ".join(done))
+ raise YunohostValidationError("migrations_already_ran", ids=", ".join(done))
# So, is there actually something to do ?
if not targets:
@@ -1101,12 +1057,68 @@ def _skip_all_migrations():
write_to_yaml(MIGRATIONS_STATE_PATH, new_states)
+def _tools_migrations_run_after_system_restore(backup_version):
+
+ all_migrations = _get_migrations_list()
+
+ current_version = version.parse(ynh_packages_version()["yunohost"]["version"])
+ backup_version = version.parse(backup_version)
+
+ if backup_version == current_version:
+ return
+
+ for migration in all_migrations:
+ if (
+ hasattr(migration, "introduced_in_version")
+ and version.parse(migration.introduced_in_version) > backup_version
+ and hasattr(migration, "run_after_system_restore")
+ ):
+ try:
+ logger.info(m18n.n("migrations_running_forward", id=migration.id))
+ migration.run_after_system_restore()
+ except Exception as e:
+ msg = m18n.n(
+ "migrations_migration_has_failed", exception=e, id=migration.id
+ )
+ logger.error(msg, exc_info=1)
+ raise
+
+
+def _tools_migrations_run_before_app_restore(backup_version, app_id):
+
+ all_migrations = _get_migrations_list()
+
+ current_version = version.parse(ynh_packages_version()["yunohost"]["version"])
+ backup_version = version.parse(backup_version)
+
+ if backup_version == current_version:
+ return
+
+ for migration in all_migrations:
+ if (
+ hasattr(migration, "introduced_in_version")
+ and version.parse(migration.introduced_in_version) > backup_version
+ and hasattr(migration, "run_before_app_restore")
+ ):
+ try:
+ logger.info(m18n.n("migrations_running_forward", id=migration.id))
+ migration.run_before_app_restore(app_id)
+ except Exception as e:
+ msg = m18n.n(
+ "migrations_migration_has_failed", exception=e, id=migration.id
+ )
+ logger.error(msg, exc_info=1)
+ raise
+
+
class Migration(object):
# Those are to be implemented by daughter classes
mode = "auto"
- dependencies = [] # List of migration ids required before running this migration
+ dependencies: List[
+ str
+ ] = [] # List of migration ids required before running this migration
@property
def disclaimer(self):
@@ -1125,3 +1137,53 @@ class Migration(object):
@property
def description(self):
return m18n.n("migration_description_%s" % self.id)
+
+ def ldap_migration(run):
+ def func(self):
+
+ # Backup LDAP before the migration
+ logger.info(m18n.n("migration_ldap_backup_before_migration"))
+ try:
+ backup_folder = "/home/yunohost.backup/premigration/" + time.strftime(
+ "%Y%m%d-%H%M%S", time.gmtime()
+ )
+ mkdir(backup_folder, 0o750, parents=True)
+ os.system("systemctl stop slapd")
+ cp("/etc/ldap", f"{backup_folder}/ldap_config", recursive=True)
+ cp("/var/lib/ldap", f"{backup_folder}/ldap_db", recursive=True)
+ cp(
+ "/etc/yunohost/apps",
+ f"{backup_folder}/apps_settings",
+ recursive=True,
+ )
+ except Exception as e:
+ raise YunohostError(
+ "migration_ldap_can_not_backup_before_migration", error=str(e)
+ )
+ finally:
+ os.system("systemctl start slapd")
+
+ try:
+ run(self, backup_folder)
+ except Exception:
+ logger.warning(
+ m18n.n("migration_ldap_migration_failed_trying_to_rollback")
+ )
+ os.system("systemctl stop slapd")
+ # To be sure that we don't keep some part of the old config
+ rm("/etc/ldap/slapd.d", force=True, recursive=True)
+ cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True)
+ cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True)
+ cp(
+ f"{backup_folder}/apps_settings",
+ "/etc/yunohost/apps",
+ recursive=True,
+ )
+ os.system("systemctl start slapd")
+ rm(backup_folder, force=True, recursive=True)
+ logger.info(m18n.n("migration_ldap_rollback_success"))
+ raise
+ else:
+ rm(backup_folder, force=True, recursive=True)
+
+ return func
diff --git a/src/yunohost/user.py b/src/yunohost/user.py
index f1fab786a..c9f70e152 100644
--- a/src/yunohost/user.py
+++ b/src/yunohost/user.py
@@ -33,43 +33,81 @@ import string
import subprocess
import copy
-from moulinette import msignals, msettings, m18n
+from moulinette import Moulinette, m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output
-from yunohost.utils.error import YunohostError
+from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.service import service_status
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",
- "loginShell": "shell",
- "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)
- else:
- raise YunohostError("field_invalid", attr)
- else:
- attrs = ["uid", "cn", "mail", "mailuserquota", "loginShell"]
+ 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", field)
ldap = _get_ldap_interface()
result = ldap.search(
@@ -80,18 +118,13 @@ def user_list(fields=None):
for user in result:
entry = {}
- for attr, values in user.items():
- if values:
- if attr == "loginShell":
- if values[0].strip() == "/bin/false":
- entry["ssh_allowed"] = False
- else:
- entry["ssh_allowed"] = True
+ 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)
- entry[user_attrs[attr]] = values[0]
-
- uid = entry[user_attrs["uid"]]
- users[uid] = entry
+ users[user["uid"][0]] = entry
return {"users": users}
@@ -106,9 +139,10 @@ def user_create(
password,
mailbox_quota="0",
mail=None,
+ from_import=False,
):
- from yunohost.domain import domain_list, _get_maindomain
+ from yunohost.domain import domain_list, _get_maindomain, _assert_domain_exists
from yunohost.hook import hook_callback
from yunohost.utils.password import assert_password_is_strong_enough
from yunohost.utils.ldap import _get_ldap_interface
@@ -124,55 +158,51 @@ def user_create(
# Validate domain used for email address/xmpp account
if domain is None:
- if msettings.get("interface") == "api":
- raise YunohostError("Invalide usage, specify domain argument")
+ if Moulinette.interface.type == "api":
+ raise YunohostValidationError(
+ "Invalid usage, you should specify a domain argument"
+ )
else:
# On affiche les differents domaines possibles
- msignals.display(m18n.n("domains_available"))
+ Moulinette.display(m18n.n("domains_available"))
for domain in domain_list()["domains"]:
- msignals.display("- {}".format(domain))
+ Moulinette.display("- {}".format(domain))
maindomain = _get_maindomain()
- domain = msignals.prompt(
+ domain = Moulinette.prompt(
m18n.n("ask_user_domain") + " (default: %s)" % maindomain
)
if not domain:
domain = maindomain
# Check that the domain exists
- if domain not in domain_list()["domains"]:
- raise YunohostError("domain_name_unknown", domain=domain)
+ _assert_domain_exists(domain)
mail = username + "@" + domain
ldap = _get_ldap_interface()
if username in user_list()["users"]:
- raise YunohostError("user_already_exists", user=username)
+ raise YunohostValidationError("user_already_exists", user=username)
# Validate uniqueness of username and mail in LDAP
try:
ldap.validate_uniqueness({"uid": username, "mail": mail, "cn": username})
except Exception as e:
- raise YunohostError("user_creation_failed", user=username, error=e)
+ raise YunohostValidationError("user_creation_failed", user=username, error=e)
# Validate uniqueness of username in system users
all_existing_usernames = {x.pw_name for x in pwd.getpwall()}
if username in all_existing_usernames:
- raise YunohostError("system_username_exists")
+ 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 YunohostError("mail_unavailable")
+ raise YunohostValidationError("mail_unavailable")
- operation_logger.start()
+ if not from_import:
+ operation_logger.start()
# Get random UID/GID
all_uid = {str(x.pw_uid) for x in pwd.getpwall()}
@@ -206,7 +236,7 @@ def user_create(
"gidNumber": [uid],
"uidNumber": [uid],
"homeDirectory": ["/home/" + username],
- "loginShell": ["/bin/false"],
+ "loginShell": ["/bin/bash"],
}
# If it is the first user, add some aliases
@@ -226,8 +256,16 @@ 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(
+ ["setfacl", "-m", "g:all_users:---", "/home/%s" % username]
+ )
+ except subprocess.CalledProcessError:
+ logger.warning("Failed to protect /home/%s" % username, exc_info=1)
# Create group for user and add to group 'all_users'
user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False)
@@ -245,13 +283,14 @@ def user_create(
hook_callback("post_user_create", args=[username, mail], env=env_dict)
# TODO: Send a welcome mail to user
- logger.success(m18n.n("user_created"))
+ 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
@@ -264,9 +303,10 @@ def user_delete(operation_logger, username, purge=False):
from yunohost.utils.ldap import _get_ldap_interface
if username not in user_list()["users"]:
- raise YunohostError("user_unknown", user=username)
+ raise YunohostValidationError("user_unknown", user=username)
- operation_logger.start()
+ if not from_import:
+ operation_logger.start()
user_group_update("all_users", remove=username, force=True, sync_perm=False)
for group, infos in user_group_list()["groups"].items():
@@ -298,7 +338,8 @@ def user_delete(operation_logger, username, purge=False):
hook_callback("post_user_delete", args=[username, purge])
- logger.success(m18n.n("user_deleted"))
+ if not from_import:
+ logger.success(m18n.n("user_deleted"))
@is_unit_operation([("username", "user")], exclude=["change_password"])
@@ -314,6 +355,7 @@ def user_update(
add_mailalias=None,
remove_mailalias=None,
mailbox_quota=None,
+ from_import=False,
):
"""
Update user informations
@@ -347,7 +389,7 @@ def user_update(
attrs=attrs_to_fetch,
)
if not result:
- raise YunohostError("user_unknown", user=username)
+ raise YunohostValidationError("user_unknown", user=username)
user = result[0]
env_dict = {"YNH_USER_USERNAME": username}
@@ -373,12 +415,14 @@ 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.
- if msettings.get("interface") == "cli" and not change_password:
- change_password = msignals.prompt(m18n.n("ask_password"), True, True)
+ if Moulinette.interface.type == "cli" and not change_password:
+ change_password = Moulinette.prompt(
+ m18n.n("ask_password"), is_password=True, confirm=True
+ )
# Ensure sufficiently complex password
assert_password_is_strong_enough("user", change_password)
@@ -387,34 +431,38 @@ def user_update(
if mail:
main_domain = _get_maindomain()
- aliases = [
- "root@" + main_domain,
- "admin@" + main_domain,
- "webmaster@" + main_domain,
- "postmaster@" + main_domain,
- ]
- try:
- ldap.validate_uniqueness({"mail": mail})
- except Exception as e:
- raise YunohostError("user_update_failed", user=username, error=e)
+ 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 YunohostError("user_update_failed", user=username, error=e)
if mail[mail.find("@") + 1 :] not in domains:
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
if mail in aliases:
- raise YunohostError("mail_unavailable")
+ 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:
- try:
- ldap.validate_uniqueness({"mail": mail})
- except Exception as e:
- raise YunohostError("user_update_failed", user=username, error=e)
+ # (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 YunohostError("user_update_failed", user=username, error=e)
if mail[mail.find("@") + 1 :] not in domains:
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
@@ -429,7 +477,7 @@ def user_update(
if len(user["mail"]) > 1 and mail in user["mail"][1:]:
user["mail"].remove(mail)
else:
- raise YunohostError("mail_alias_remove_failed", mail=mail)
+ raise YunohostValidationError("mail_alias_remove_failed", mail=mail)
new_attr_dict["mail"] = user["mail"]
if "mail" in new_attr_dict:
@@ -451,7 +499,7 @@ def user_update(
if len(user["maildrop"]) > 1 and mail in user["maildrop"][1:]:
user["maildrop"].remove(mail)
else:
- raise YunohostError("mail_forward_remove_failed", mail=mail)
+ raise YunohostValidationError("mail_forward_remove_failed", mail=mail)
new_attr_dict["maildrop"] = user["maildrop"]
if "maildrop" in new_attr_dict:
@@ -461,7 +509,8 @@ def user_update(
new_attr_dict["mailuserquota"] = [mailbox_quota]
env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota
- operation_logger.start()
+ if not from_import:
+ operation_logger.start()
try:
ldap.update("uid=%s,ou=users" % username, new_attr_dict)
@@ -471,9 +520,10 @@ def user_update(
# Trigger post_user_update hooks
hook_callback("post_user_update", env=env_dict)
- logger.success(m18n.n("user_updated"))
- app_ssowatconf()
- return user_info(username)
+ if not from_import:
+ app_ssowatconf()
+ logger.success(m18n.n("user_updated"))
+ return user_info(username)
def user_info(username):
@@ -500,7 +550,7 @@ def user_info(username):
if result:
user = result[0]
else:
- raise YunohostError("user_unknown", user=username)
+ raise YunohostValidationError("user_unknown", user=username)
result_dict = {
"username": user["uid"][0],
@@ -508,6 +558,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:
@@ -562,6 +614,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 = [element.strip() for element 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
#
@@ -638,7 +999,7 @@ def user_group_create(
{"cn": groupname}, base_dn="ou=groups,dc=yunohost,dc=org"
)
if conflict:
- raise YunohostError("group_already_exist", group=groupname)
+ raise YunohostValidationError("group_already_exist", group=groupname)
# Validate uniqueness of groupname in system group
all_existing_groupnames = {x.gr_name for x in grp.getgrall()}
@@ -651,7 +1012,9 @@ def user_group_create(
"sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True
)
else:
- raise YunohostError("group_already_exist_on_system", group=groupname)
+ raise YunohostValidationError(
+ "group_already_exist_on_system", group=groupname
+ )
if not gid:
# Get random GID
@@ -705,7 +1068,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True):
existing_groups = list(user_group_list()["groups"].keys())
if groupname not in existing_groups:
- raise YunohostError("group_unknown", group=groupname)
+ raise YunohostValidationError("group_unknown", group=groupname)
# Refuse to delete primary groups of a user (e.g. group 'sam' related to user 'sam')
# without the force option...
@@ -714,7 +1077,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True):
existing_users = list(user_list()["users"].keys())
undeletable_groups = existing_users + ["all_users", "visitors"]
if groupname in undeletable_groups and not force:
- raise YunohostError("group_cannot_be_deleted", group=groupname)
+ raise YunohostValidationError("group_cannot_be_deleted", group=groupname)
operation_logger.start()
ldap = _get_ldap_interface()
@@ -734,7 +1097,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
@@ -756,11 +1125,13 @@ def user_group_update(
# We also can't edit "all_users" without the force option because that's a special group...
if not force:
if groupname == "all_users":
- raise YunohostError("group_cannot_edit_all_users")
+ raise YunohostValidationError("group_cannot_edit_all_users")
elif groupname == "visitors":
- raise YunohostError("group_cannot_edit_visitors")
+ raise YunohostValidationError("group_cannot_edit_visitors")
elif groupname in existing_users:
- raise YunohostError("group_cannot_edit_primary_group", group=groupname)
+ raise YunohostValidationError(
+ "group_cannot_edit_primary_group", group=groupname
+ )
# We extract the uid for each member of the group to keep a simple flat list of members
current_group = user_group_info(groupname)["members"]
@@ -771,7 +1142,7 @@ def user_group_update(
for user in users_to_add:
if user not in existing_users:
- raise YunohostError("user_unknown", user=user)
+ raise YunohostValidationError("user_unknown", user=user)
if user in current_group:
logger.warning(
@@ -802,7 +1173,8 @@ def user_group_update(
]
if set(new_group) != set(current_group):
- operation_logger.start()
+ if not from_import:
+ operation_logger.start()
ldap = _get_ldap_interface()
try:
ldap.update(
@@ -812,14 +1184,16 @@ def user_group_update(
except Exception as e:
raise YunohostError("group_update_failed", group=groupname, error=e)
- 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)
+
+ 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))
+
+ return user_group_info(groupname)
def user_group_info(groupname):
@@ -843,7 +1217,7 @@ def user_group_info(groupname):
)
if not result:
- raise YunohostError("group_unknown", group=groupname)
+ raise YunohostValidationError("group_unknown", group=groupname)
infos = result[0]
@@ -857,29 +1231,68 @@ def user_group_info(groupname):
}
+def user_group_add(groupname, usernames, force=False, sync_perm=True):
+ """
+ Add user(s) to a group
+
+ Keyword argument:
+ groupname -- Groupname to update
+ usernames -- User(s) to add in the group
+
+ """
+ return user_group_update(groupname, add=usernames, force=force, sync_perm=sync_perm)
+
+
+def user_group_remove(groupname, usernames, force=False, sync_perm=True):
+ """
+ Remove user(s) from a group
+
+ Keyword argument:
+ groupname -- Groupname to update
+ usernames -- User(s) to remove from the group
+
+ """
+ return user_group_update(
+ groupname, remove=usernames, force=force, sync_perm=sync_perm
+ )
+
+
#
# Permission subcategory
#
-def user_permission_list(short=False, full=False):
+def user_permission_list(short=False, full=False, apps=[]):
import yunohost.permission
- return yunohost.permission.user_permission_list(short, full, absolute_urls=True)
+ return yunohost.permission.user_permission_list(
+ short, full, absolute_urls=True, apps=apps
+ )
-def user_permission_update(
- permission, add=None, remove=None, label=None, show_tile=None, sync_perm=True
+def user_permission_update(permission, label=None, show_tile=None, sync_perm=True):
+ import yunohost.permission
+
+ return yunohost.permission.user_permission_update(
+ permission, label=label, show_tile=show_tile, sync_perm=sync_perm
+ )
+
+
+def user_permission_add(permission, names, protected=None, force=False, sync_perm=True):
+ import yunohost.permission
+
+ return yunohost.permission.user_permission_update(
+ permission, add=names, protected=protected, force=force, sync_perm=sync_perm
+ )
+
+
+def user_permission_remove(
+ permission, names, protected=None, force=False, sync_perm=True
):
import yunohost.permission
return yunohost.permission.user_permission_update(
- permission,
- add=add,
- remove=remove,
- label=label,
- show_tile=show_tile,
- sync_perm=sync_perm,
+ permission, remove=names, protected=protected, force=force, sync_perm=sync_perm
)
@@ -901,14 +1314,6 @@ def user_permission_info(permission):
import yunohost.ssh
-def user_ssh_allow(username):
- return yunohost.ssh.user_ssh_allow(username)
-
-
-def user_ssh_disallow(username):
- return yunohost.ssh.user_ssh_disallow(username)
-
-
def user_ssh_list_keys(username):
return yunohost.ssh.user_ssh_list_keys(username)
diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py
new file mode 100644
index 000000000..4ee62c6f7
--- /dev/null
+++ b/src/yunohost/utils/config.py
@@ -0,0 +1,1253 @@
+# -*- coding: utf-8 -*-
+
+""" License
+
+ Copyright (C) 2018 YUNOHOST.ORG
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses
+
+"""
+
+import os
+import re
+import urllib.parse
+import tempfile
+import shutil
+import ast
+import operator as op
+from collections import OrderedDict
+from typing import Optional, Dict, List, Union, Any, Mapping
+
+from moulinette.interfaces.cli import colorize
+from moulinette import Moulinette, m18n
+from moulinette.utils.log import getActionLogger
+from moulinette.utils.filesystem import (
+ read_file,
+ write_to_file,
+ read_toml,
+ read_yaml,
+ write_to_yaml,
+ mkdir,
+)
+
+from yunohost.utils.i18n import _value_for_locale
+from yunohost.utils.error import YunohostError, YunohostValidationError
+from yunohost.log import OperationLogger
+
+logger = getActionLogger("yunohost.config")
+CONFIG_PANEL_VERSION_SUPPORTED = 1.0
+
+# Those js-like evaluate functions are used to eval safely visible attributes
+# The goal is to evaluate in the same way than js simple-evaluate
+# https://github.com/shepherdwind/simple-evaluate
+def evaluate_simple_ast(node, context={}):
+ operators = {
+ ast.Not: op.not_,
+ ast.Mult: op.mul,
+ ast.Div: op.truediv, # number
+ ast.Mod: op.mod, # number
+ ast.Add: op.add, # str
+ ast.Sub: op.sub, # number
+ ast.USub: op.neg, # Negative number
+ ast.Gt: op.gt,
+ ast.Lt: op.lt,
+ ast.GtE: op.ge,
+ ast.LtE: op.le,
+ ast.Eq: op.eq,
+ ast.NotEq: op.ne,
+ }
+ context["true"] = True
+ context["false"] = False
+ context["null"] = None
+
+ # Variable
+ if isinstance(node, ast.Name): # Variable
+ return context[node.id]
+
+ # Python <=3.7 String
+ elif isinstance(node, ast.Str):
+ return node.s
+
+ # Python <=3.7 Number
+ elif isinstance(node, ast.Num):
+ return node.n
+
+ # Boolean, None and Python 3.8 for Number, Boolean, String and None
+ elif isinstance(node, (ast.Constant, ast.NameConstant)):
+ return node.value
+
+ # + - * / %
+ elif (
+ isinstance(node, ast.BinOp) and type(node.op) in operators
+ ): #
+ left = evaluate_simple_ast(node.left, context)
+ right = evaluate_simple_ast(node.right, context)
+ if type(node.op) == ast.Add:
+ if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42
+ left = str(left)
+ right = str(right)
+ elif type(left) != type(right): # support "111" - "1" -> 110
+ left = float(left)
+ right = float(right)
+
+ return operators[type(node.op)](left, right)
+
+ # Comparison
+ # JS and Python don't give the same result for multi operators
+ # like True == 10 > 2.
+ elif (
+ isinstance(node, ast.Compare) and len(node.comparators) == 1
+ ): #
+ left = evaluate_simple_ast(node.left, context)
+ right = evaluate_simple_ast(node.comparators[0], context)
+ operator = node.ops[0]
+ if isinstance(left, (int, float)) or isinstance(right, (int, float)):
+ try:
+ left = float(left)
+ right = float(right)
+ except ValueError:
+ return type(operator) == ast.NotEq
+ try:
+ return operators[type(operator)](left, right)
+ except TypeError: # support "e" > 1 -> False like in JS
+ return False
+
+ # and / or
+ elif isinstance(node, ast.BoolOp): #
+ for value in node.values:
+ value = evaluate_simple_ast(value, context)
+ if isinstance(node.op, ast.And) and not value:
+ return False
+ elif isinstance(node.op, ast.Or) and value:
+ return True
+ return isinstance(node.op, ast.And)
+
+ # not / USub (it's negation number -\d)
+ elif isinstance(node, ast.UnaryOp): # e.g., -1
+ return operators[type(node.op)](evaluate_simple_ast(node.operand, context))
+
+ # match function call
+ elif isinstance(node, ast.Call) and node.func.__dict__.get("id") == "match":
+ return re.match(
+ evaluate_simple_ast(node.args[1], context), context[node.args[0].id]
+ )
+
+ # Unauthorized opcode
+ else:
+ opcode = str(type(node))
+ raise YunohostError(
+ f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True
+ )
+
+
+def js_to_python(expr):
+ in_string = None
+ py_expr = ""
+ i = 0
+ escaped = False
+ for char in expr:
+ if char in r"\"'":
+ # Start a string
+ if not in_string:
+ in_string = char
+
+ # Finish a string
+ elif in_string == char and not escaped:
+ in_string = None
+
+ # If we are not in a string, replace operators
+ elif not in_string:
+ if char == "!" and expr[i + 1] != "=":
+ char = "not "
+ elif char in "|&" and py_expr[-1:] == char:
+ py_expr = py_expr[:-1]
+ char = " and " if char == "&" else " or "
+
+ # Determine if next loop will be in escaped mode
+ escaped = char == "\\" and not escaped
+ py_expr += char
+ i += 1
+ return py_expr
+
+
+def evaluate_simple_js_expression(expr, context={}):
+ if not expr.strip():
+ return False
+ node = ast.parse(js_to_python(expr), mode="eval").body
+ return evaluate_simple_ast(node, context)
+
+
+class ConfigPanel:
+ def __init__(self, config_path, save_path=None):
+ self.config_path = config_path
+ self.save_path = save_path
+ self.config = {}
+ self.values = {}
+ self.new_values = {}
+
+ def get(self, key="", mode="classic"):
+ self.filter_key = key or ""
+
+ # Read config panel toml
+ self._get_config_panel()
+
+ if not self.config:
+ raise YunohostValidationError("config_no_panel")
+
+ # Read or get values and hydrate the config
+ self._load_current_values()
+ self._hydrate()
+
+ # In 'classic' mode, we display the current value if key refer to an option
+ if self.filter_key.count(".") == 2 and mode == "classic":
+ option = self.filter_key.split(".")[-1]
+ return self.values.get(option, None)
+
+ # Format result in 'classic' or 'export' mode
+ logger.debug(f"Formating result in '{mode}' mode")
+ result = {}
+ for panel, section, option in self._iterate():
+ key = f"{panel['id']}.{section['id']}.{option['id']}"
+ if mode == "export":
+ result[option["id"]] = option.get("current_value")
+ continue
+
+ ask = None
+ if "ask" in option:
+ ask = _value_for_locale(option["ask"])
+ elif "i18n" in self.config:
+ ask = m18n.n(self.config["i18n"] + "_" + option["id"])
+
+ if mode == "full":
+ # edit self.config directly
+ option["ask"] = ask
+ else:
+ result[key] = {"ask": ask}
+ if "current_value" in option:
+ question_class = ARGUMENTS_TYPE_PARSERS[
+ option.get("type", "string")
+ ]
+ result[key]["value"] = question_class.humanize(
+ option["current_value"], option
+ )
+ # FIXME: semantics, technically here this is not about a prompt...
+ if question_class.hide_user_input_in_prompt:
+ result[key][
+ "value"
+ ] = "**************" # Prevent displaying password in `config get`
+
+ if mode == "full":
+ return self.config
+ else:
+ return result
+
+ def set(
+ self, key=None, value=None, args=None, args_file=None, operation_logger=None
+ ):
+ self.filter_key = key or ""
+
+ # Read config panel toml
+ self._get_config_panel()
+
+ if not self.config:
+ raise YunohostValidationError("config_no_panel")
+
+ if (args is not None or args_file is not None) and value is not None:
+ raise YunohostValidationError(
+ "You should either provide a value, or a serie of args/args_file, but not both at the same time",
+ raw_msg=True,
+ )
+
+ if self.filter_key.count(".") != 2 and value is not None:
+ raise YunohostValidationError("config_cant_set_value_on_section")
+
+ # Import and parse pre-answered options
+ logger.debug("Import and parse pre-answered options")
+ args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
+ self.args = {key: ",".join(value_) for key, value_ in args.items()}
+
+ if args_file:
+ # Import YAML / JSON file but keep --args values
+ self.args = {**read_yaml(args_file), **self.args}
+
+ if value is not None:
+ self.args = {self.filter_key.split(".")[-1]: value}
+
+ # Read or get values and hydrate the config
+ self._load_current_values()
+ self._hydrate()
+ self._ask()
+
+ if operation_logger:
+ operation_logger.start()
+
+ try:
+ self._apply()
+ except YunohostError:
+ raise
+ # Script got manually interrupted ...
+ # N.B. : KeyboardInterrupt does not inherit from Exception
+ except (KeyboardInterrupt, EOFError):
+ error = m18n.n("operation_interrupted")
+ logger.error(m18n.n("config_apply_failed", error=error))
+ raise
+ # Something wrong happened in Yunohost's code (most probably hook_exec)
+ except Exception:
+ import traceback
+
+ error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
+ logger.error(m18n.n("config_apply_failed", error=error))
+ raise
+ finally:
+ # Delete files uploaded from API
+ # FIXME : this is currently done in the context of config panels,
+ # but could also happen in the context of app install ... (or anywhere else
+ # where we may parse args etc...)
+ FileQuestion.clean_upload_dirs()
+
+ self._reload_services()
+
+ logger.success("Config updated as expected")
+ operation_logger.success()
+
+ def _get_toml(self):
+ return read_toml(self.config_path)
+
+ def _get_config_panel(self):
+
+ # Split filter_key
+ filter_key = self.filter_key.split(".") if self.filter_key != "" else []
+ if len(filter_key) > 3:
+ raise YunohostError(
+ f"The filter key {filter_key} has too many sub-levels, the max is 3.",
+ raw_msg=True,
+ )
+
+ if not os.path.exists(self.config_path):
+ logger.debug(f"Config panel {self.config_path} doesn't exists")
+ return None
+
+ toml_config_panel = self._get_toml()
+
+ # Check TOML config panel is in a supported version
+ if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED:
+ raise YunohostError(
+ "config_version_not_supported", version=toml_config_panel["version"]
+ )
+
+ # Transform toml format into internal format
+ format_description = {
+ "root": {
+ "properties": ["version", "i18n"],
+ "defaults": {"version": 1.0},
+ },
+ "panels": {
+ "properties": ["name", "services", "actions", "help"],
+ "defaults": {
+ "services": [],
+ "actions": {"apply": {"en": "Apply"}},
+ },
+ },
+ "sections": {
+ "properties": ["name", "services", "optional", "help", "visible"],
+ "defaults": {
+ "name": "",
+ "services": [],
+ "optional": True,
+ },
+ },
+ "options": {
+ "properties": [
+ "ask",
+ "type",
+ "bind",
+ "help",
+ "example",
+ "default",
+ "style",
+ "icon",
+ "placeholder",
+ "visible",
+ "optional",
+ "choices",
+ "yes",
+ "no",
+ "pattern",
+ "limit",
+ "min",
+ "max",
+ "step",
+ "accept",
+ "redact",
+ ],
+ "defaults": {},
+ },
+ }
+
+ def _build_internal_config_panel(raw_infos, level):
+ """Convert TOML in internal format ('full' mode used by webadmin)
+ Here are some properties of 1.0 config panel in toml:
+ - node properties and node children are mixed,
+ - text are in english only
+ - some properties have default values
+ This function detects all children nodes and put them in a list
+ """
+
+ defaults = format_description[level]["defaults"]
+ properties = format_description[level]["properties"]
+
+ # Start building the ouput (merging the raw infos + defaults)
+ out = {key: raw_infos.get(key, value) for key, value in defaults.items()}
+
+ # Now fill the sublevels (+ apply filter_key)
+ i = list(format_description).index(level)
+ sublevel = list(format_description)[i + 1] if level != "options" else None
+ search_key = filter_key[i] if len(filter_key) > i else False
+
+ for key, value in raw_infos.items():
+ # Key/value are a child node
+ if (
+ isinstance(value, OrderedDict)
+ and key not in properties
+ and sublevel
+ ):
+ # We exclude all nodes not referenced by the filter_key
+ if search_key and key != search_key:
+ continue
+ subnode = _build_internal_config_panel(value, sublevel)
+ subnode["id"] = key
+ if level == "root":
+ subnode.setdefault("name", {"en": key.capitalize()})
+ elif level == "sections":
+ subnode["name"] = key # legacy
+ subnode.setdefault("optional", raw_infos.get("optional", True))
+ out.setdefault(sublevel, []).append(subnode)
+ # Key/value are a property
+ else:
+ if key not in properties:
+ logger.warning(f"Unknown key '{key}' found in config panel")
+ # Todo search all i18n keys
+ out[key] = (
+ value if key not in ["ask", "help", "name"] else {"en": value}
+ )
+ return out
+
+ self.config = _build_internal_config_panel(toml_config_panel, "root")
+
+ try:
+ self.config["panels"][0]["sections"][0]["options"][0]
+ except (KeyError, IndexError):
+ raise YunohostValidationError(
+ "config_unknown_filter_key", filter_key=self.filter_key
+ )
+
+ # List forbidden keywords from helpers and sections toml (to avoid conflict)
+ forbidden_keywords = [
+ "old",
+ "app",
+ "changed",
+ "file_hash",
+ "binds",
+ "types",
+ "formats",
+ "getter",
+ "setter",
+ "short_setting",
+ "type",
+ "bind",
+ "nothing_changed",
+ "changes_validated",
+ "result",
+ "max_progression",
+ ]
+ forbidden_keywords += format_description["sections"]
+
+ for _, _, option in self._iterate():
+ if option["id"] in forbidden_keywords:
+ raise YunohostError("config_forbidden_keyword", keyword=option["id"])
+ return self.config
+
+ def _hydrate(self):
+ # Hydrating config panel with current value
+ logger.debug("Hydrating config with current values")
+ for _, _, option in self._iterate():
+ if option["id"] not in self.values:
+ allowed_empty_types = ["alert", "display_text", "markdown", "file"]
+ if (
+ option["type"] in allowed_empty_types
+ or option.get("bind") == "null"
+ ):
+ continue
+ else:
+ raise YunohostError(
+ f"Config panel question '{option['id']}' should be initialized with a value during install or upgrade.",
+ raw_msg=True,
+ )
+ value = self.values[option["name"]]
+ # In general, the value is just a simple value.
+ # Sometimes it could be a dict used to overwrite the option itself
+ value = value if isinstance(value, dict) else {"current_value": value}
+ option.update(value)
+
+ return self.values
+
+ def _ask(self):
+ logger.debug("Ask unanswered question and prevalidate data")
+
+ if "i18n" in self.config:
+ for panel, section, option in self._iterate():
+ if "ask" not in option:
+ option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"])
+
+ def display_header(message):
+ """CLI panel/section header display"""
+ if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2:
+ Moulinette.display(colorize(message, "purple"))
+
+ for panel, section, obj in self._iterate(["panel", "section"]):
+ if panel == obj:
+ name = _value_for_locale(panel["name"])
+ display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}")
+ continue
+ name = _value_for_locale(section["name"])
+ if name:
+ display_header(f"\n# {name}")
+
+ # Check and ask unanswered questions
+ questions = ask_questions_and_parse_answers(section["options"], self.args)
+ self.new_values.update(
+ {
+ question.name: question.value
+ for question in questions
+ if question.value is not None
+ }
+ )
+
+ self.errors = None
+
+ def _get_default_values(self):
+ return {
+ option["id"]: option["default"]
+ for _, _, option in self._iterate()
+ if "default" in option
+ }
+
+ def _load_current_values(self):
+ """
+ Retrieve entries in YAML file
+ And set default values if needed
+ """
+
+ # Retrieve entries in the YAML
+ on_disk_settings = {}
+ if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
+ on_disk_settings = read_yaml(self.save_path) or {}
+
+ # Inject defaults if needed (using the magic .update() ;))
+ self.values = self._get_default_values()
+ self.values.update(on_disk_settings)
+
+ def _apply(self):
+ logger.info("Saving the new configuration...")
+ dir_path = os.path.dirname(os.path.realpath(self.save_path))
+ if not os.path.exists(dir_path):
+ mkdir(dir_path, mode=0o700)
+
+ values_to_save = {**self.values, **self.new_values}
+ if self.save_mode == "diff":
+ defaults = self._get_default_values()
+ values_to_save = {
+ k: v for k, v in values_to_save.items() if defaults.get(k) != v
+ }
+
+ # Save the settings to the .yaml file
+ write_to_yaml(self.save_path, values_to_save)
+
+ def _reload_services(self):
+
+ from yunohost.service import service_reload_or_restart
+
+ services_to_reload = set()
+ for panel, section, obj in self._iterate(["panel", "section", "option"]):
+ services_to_reload |= set(obj.get("services", []))
+
+ services_to_reload = list(services_to_reload)
+ services_to_reload.sort(key="nginx".__eq__)
+ if services_to_reload:
+ logger.info("Reloading services...")
+ for service in services_to_reload:
+ if hasattr(self, "app"):
+ service = service.replace("__APP__", self.app)
+ service_reload_or_restart(service)
+
+ def _iterate(self, trigger=["option"]):
+ for panel in self.config.get("panels", []):
+ if "panel" in trigger:
+ yield (panel, None, panel)
+ for section in panel.get("sections", []):
+ if "section" in trigger:
+ yield (panel, section, section)
+ if "option" in trigger:
+ for option in section.get("options", []):
+ yield (panel, section, option)
+
+
+class Question(object):
+ hide_user_input_in_prompt = False
+ pattern: Optional[Dict] = None
+
+ def __init__(self, question: Dict[str, Any], context: Mapping[str, Any] = {}):
+ self.name = question["name"]
+ self.type = question.get("type", "string")
+ self.default = question.get("default", None)
+ self.optional = question.get("optional", False)
+ self.visible = question.get("visible", None)
+ self.context = context
+ self.choices = question.get("choices", [])
+ self.pattern = question.get("pattern", self.pattern)
+ self.ask = question.get("ask", {"en": self.name})
+ self.help = question.get("help")
+ self.redact = question.get("redact", False)
+ # .current_value is the currently stored value
+ self.current_value = question.get("current_value")
+ # .value is the "proposed" value which we got from the user
+ self.value = question.get("value")
+
+ # Empty value is parsed as empty string
+ if self.default == "":
+ self.default = None
+
+ @staticmethod
+ def humanize(value, option={}):
+ return str(value)
+
+ @staticmethod
+ def normalize(value, option={}):
+ if isinstance(value, str):
+ value = value.strip()
+ return value
+
+ def _prompt(self, text):
+ prefill = ""
+ if self.current_value is not None:
+ prefill = self.humanize(self.current_value, self)
+ elif self.default is not None:
+ prefill = self.humanize(self.default, self)
+ self.value = Moulinette.prompt(
+ message=text,
+ is_password=self.hide_user_input_in_prompt,
+ confirm=False,
+ prefill=prefill,
+ is_multiline=(self.type == "text"),
+ autocomplete=self.choices,
+ help=_value_for_locale(self.help),
+ )
+
+ def ask_if_needed(self):
+
+ if self.visible and not evaluate_simple_js_expression(
+ self.visible, context=self.context
+ ):
+ # FIXME There could be several use case if the question is not displayed:
+ # - we doesn't want to give a specific value
+ # - we want to keep the previous value
+ # - we want the default value
+ self.value = None
+ return self.value
+
+ for i in range(5):
+ # Display question if no value filled or if it's a readonly message
+ if Moulinette.interface.type == "cli" and os.isatty(1):
+ text_for_user_input_in_cli = self._format_text_for_user_input_in_cli()
+ if getattr(self, "readonly", False):
+ Moulinette.display(text_for_user_input_in_cli)
+ elif self.value is None:
+ self._prompt(text_for_user_input_in_cli)
+
+ # Apply default value
+ class_default = getattr(self, "default_value", None)
+ if self.value in [None, ""] and (
+ self.default is not None or class_default is not None
+ ):
+ self.value = class_default if self.default is None else self.default
+
+ try:
+ # Normalize and validate
+ self.value = self.normalize(self.value, self)
+ self._prevalidate()
+ except YunohostValidationError as e:
+ # If in interactive cli, re-ask the current question
+ if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1):
+ logger.error(str(e))
+ self.value = None
+ continue
+
+ # Otherwise raise the ValidationError
+ raise
+
+ break
+
+ self.value = self._post_parse_value()
+
+ return self.value
+
+ def _prevalidate(self):
+ if self.value in [None, ""] and not self.optional:
+ raise YunohostValidationError("app_argument_required", name=self.name)
+
+ # we have an answer, do some post checks
+ if self.value not in [None, ""]:
+ if self.choices and self.value not in self.choices:
+ raise YunohostValidationError(
+ "app_argument_choice_invalid",
+ name=self.name,
+ value=self.value,
+ choices=", ".join(self.choices),
+ )
+ if self.pattern and not re.match(self.pattern["regexp"], str(self.value)):
+ raise YunohostValidationError(
+ self.pattern["error"],
+ name=self.name,
+ value=self.value,
+ )
+
+ def _format_text_for_user_input_in_cli(self):
+
+ text_for_user_input_in_cli = _value_for_locale(self.ask)
+
+ if self.choices:
+
+ # Prevent displaying a shitload of choices
+ # (e.g. 100+ available users when choosing an app admin...)
+ choices = (
+ list(self.choices.keys())
+ if isinstance(self.choices, dict)
+ else self.choices
+ )
+ choices_to_display = choices[:20]
+ remaining_choices = len(choices[20:])
+
+ if remaining_choices > 0:
+ choices_to_display += [
+ m18n.n("other_available_options", n=remaining_choices)
+ ]
+
+ choices_to_display = " | ".join(choices_to_display)
+
+ text_for_user_input_in_cli += f" [{choices_to_display}]"
+
+ return text_for_user_input_in_cli
+
+ def _post_parse_value(self):
+ if not self.redact:
+ return self.value
+
+ # Tell the operation_logger to redact all password-type / secret args
+ # Also redact the % escaped version of the password that might appear in
+ # the 'args' section of metadata (relevant for password with non-alphanumeric char)
+ data_to_redact = []
+ if self.value and isinstance(self.value, str):
+ data_to_redact.append(self.value)
+ if self.current_value and isinstance(self.current_value, str):
+ data_to_redact.append(self.current_value)
+ data_to_redact += [
+ urllib.parse.quote(data)
+ for data in data_to_redact
+ if urllib.parse.quote(data) != data
+ ]
+
+ for operation_logger in OperationLogger._instances:
+ operation_logger.data_to_redact.extend(data_to_redact)
+
+ return self.value
+
+
+class StringQuestion(Question):
+ argument_type = "string"
+ default_value = ""
+
+
+class EmailQuestion(StringQuestion):
+ pattern = {
+ "regexp": r"^.+@.+",
+ "error": "config_validate_email", # i18n: config_validate_email
+ }
+
+
+class URLQuestion(StringQuestion):
+ pattern = {
+ "regexp": r"^https?://.*$",
+ "error": "config_validate_url", # i18n: config_validate_url
+ }
+
+
+class DateQuestion(StringQuestion):
+ pattern = {
+ "regexp": r"^\d{4}-\d\d-\d\d$",
+ "error": "config_validate_date", # i18n: config_validate_date
+ }
+
+ def _prevalidate(self):
+ from datetime import datetime
+
+ super()._prevalidate()
+
+ if self.value not in [None, ""]:
+ try:
+ datetime.strptime(self.value, "%Y-%m-%d")
+ except ValueError:
+ raise YunohostValidationError("config_validate_date")
+
+
+class TimeQuestion(StringQuestion):
+ pattern = {
+ "regexp": r"^(1[12]|0?\d):[0-5]\d$",
+ "error": "config_validate_time", # i18n: config_validate_time
+ }
+
+
+class ColorQuestion(StringQuestion):
+ pattern = {
+ "regexp": r"^#[ABCDEFabcdef\d]{3,6}$",
+ "error": "config_validate_color", # i18n: config_validate_color
+ }
+
+
+class TagsQuestion(Question):
+ argument_type = "tags"
+
+ @staticmethod
+ def humanize(value, option={}):
+ if isinstance(value, list):
+ return ",".join(value)
+ return value
+
+ @staticmethod
+ def normalize(value, option={}):
+ if isinstance(value, list):
+ return ",".join(value)
+ if isinstance(value, str):
+ value = value.strip()
+ return value
+
+ def _prevalidate(self):
+ values = self.value
+ if isinstance(values, str):
+ values = values.split(",")
+ elif values is None:
+ values = []
+ for value in values:
+ self.value = value
+ super()._prevalidate()
+ self.value = values
+
+ def _post_parse_value(self):
+ if isinstance(self.value, list):
+ self.value = ",".join(self.value)
+ return super()._post_parse_value()
+
+
+class PasswordQuestion(Question):
+ hide_user_input_in_prompt = True
+ argument_type = "password"
+ default_value = ""
+ forbidden_chars = "{}"
+
+ def __init__(self, question, context: Mapping[str, Any] = {}):
+ super().__init__(question, context)
+ self.redact = True
+ if self.default is not None:
+ raise YunohostValidationError(
+ "app_argument_password_no_default", name=self.name
+ )
+
+ def _prevalidate(self):
+ super()._prevalidate()
+
+ if self.value not in [None, ""]:
+ if any(char in self.value for char in self.forbidden_chars):
+ raise YunohostValidationError(
+ "pattern_password_app", forbidden_chars=self.forbidden_chars
+ )
+
+ # If it's an optional argument the value should be empty or strong enough
+ from yunohost.utils.password import assert_password_is_strong_enough
+
+ assert_password_is_strong_enough("user", self.value)
+
+
+class PathQuestion(Question):
+ argument_type = "path"
+ default_value = ""
+
+ @staticmethod
+ def normalize(value, option={}):
+
+ option = option.__dict__ if isinstance(option, Question) else option
+
+ if not value.strip():
+ if option.get("optional"):
+ return ""
+ # Hmpf here we could just have a "else" case
+ # but we also want PathQuestion.normalize("") to return "/"
+ # (i.e. if no option is provided, hence .get("optional") is None
+ elif option.get("optional") is False:
+ raise YunohostValidationError(
+ "app_argument_invalid",
+ name=option.get("name"),
+ error="Question is mandatory",
+ )
+
+ return "/" + value.strip().strip(" /")
+
+
+class BooleanQuestion(Question):
+ argument_type = "boolean"
+ default_value = 0
+ yes_answers = ["1", "yes", "y", "true", "t", "on"]
+ no_answers = ["0", "no", "n", "false", "f", "off"]
+
+ @staticmethod
+ def humanize(value, option={}):
+
+ option = option.__dict__ if isinstance(option, Question) else option
+
+ yes = option.get("yes", 1)
+ no = option.get("no", 0)
+
+ value = BooleanQuestion.normalize(value, option)
+
+ if value == yes:
+ return "yes"
+ if value == no:
+ return "no"
+ if value is None:
+ return ""
+
+ raise YunohostValidationError(
+ "app_argument_choice_invalid",
+ name=option.get("name"),
+ value=value,
+ choices="yes/no",
+ )
+
+ @staticmethod
+ def normalize(value, option={}):
+
+ option = option.__dict__ if isinstance(option, Question) else option
+
+ if isinstance(value, str):
+ value = value.strip()
+
+ technical_yes = option.get("yes", 1)
+ technical_no = option.get("no", 0)
+
+ no_answers = BooleanQuestion.no_answers
+ yes_answers = BooleanQuestion.yes_answers
+
+ assert (
+ str(technical_yes).lower() not in no_answers
+ ), f"'yes' value can't be in {no_answers}"
+ assert (
+ str(technical_no).lower() not in yes_answers
+ ), f"'no' value can't be in {yes_answers}"
+
+ no_answers += [str(technical_no).lower()]
+ yes_answers += [str(technical_yes).lower()]
+
+ strvalue = str(value).lower()
+
+ if strvalue in yes_answers:
+ return technical_yes
+ if strvalue in no_answers:
+ return technical_no
+
+ if strvalue in ["none", ""]:
+ return None
+
+ raise YunohostValidationError(
+ "app_argument_choice_invalid",
+ name=option.get("name"),
+ value=strvalue,
+ choices="yes/no",
+ )
+
+ def __init__(self, question, context: Mapping[str, Any] = {}):
+ super().__init__(question, context)
+ self.yes = question.get("yes", 1)
+ self.no = question.get("no", 0)
+ if self.default is None:
+ self.default = self.no
+
+ def _format_text_for_user_input_in_cli(self):
+ text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli()
+
+ text_for_user_input_in_cli += " [yes | no]"
+
+ return text_for_user_input_in_cli
+
+ def get(self, key, default=None):
+ return getattr(self, key, default)
+
+
+class DomainQuestion(Question):
+ argument_type = "domain"
+
+ def __init__(self, question, context: Mapping[str, Any] = {}):
+ from yunohost.domain import domain_list, _get_maindomain
+
+ super().__init__(question, context)
+
+ if self.default is None:
+ self.default = _get_maindomain()
+
+ self.choices = domain_list()["domains"]
+
+ @staticmethod
+ def normalize(value, option={}):
+ if value.startswith("https://"):
+ value = value[len("https://") :]
+ elif value.startswith("http://"):
+ value = value[len("http://") :]
+
+ # Remove trailing slashes
+ value = value.rstrip("/").lower()
+
+ return value
+
+
+class UserQuestion(Question):
+ argument_type = "user"
+
+ def __init__(self, question, context: Mapping[str, Any] = {}):
+ from yunohost.user import user_list, user_info
+ from yunohost.domain import _get_maindomain
+
+ super().__init__(question, context)
+ self.choices = list(user_list()["users"].keys())
+
+ if not self.choices:
+ raise YunohostValidationError(
+ "app_argument_invalid",
+ name=self.name,
+ error="You should create a YunoHost user first.",
+ )
+
+ if self.default is None:
+ root_mail = "root@%s" % _get_maindomain()
+ for user in self.choices:
+ if root_mail in user_info(user).get("mail-aliases", []):
+ self.default = user
+ break
+
+
+class NumberQuestion(Question):
+ argument_type = "number"
+ default_value = None
+
+ def __init__(self, question, context: Mapping[str, Any] = {}):
+ super().__init__(question, context)
+ self.min = question.get("min", None)
+ self.max = question.get("max", None)
+ self.step = question.get("step", None)
+
+ @staticmethod
+ def normalize(value, option={}):
+
+ if isinstance(value, int):
+ return value
+
+ if isinstance(value, str):
+ value = value.strip()
+
+ if isinstance(value, str) and value.isdigit():
+ return int(value)
+
+ if value in [None, ""]:
+ return value
+
+ option = option.__dict__ if isinstance(option, Question) else option
+ raise YunohostValidationError(
+ "app_argument_invalid",
+ name=option.get("name"),
+ error=m18n.n("invalid_number"),
+ )
+
+ def _prevalidate(self):
+ super()._prevalidate()
+ if self.value in [None, ""]:
+ return
+
+ if self.min is not None and int(self.value) < self.min:
+ raise YunohostValidationError(
+ "app_argument_invalid",
+ name=self.name,
+ error=m18n.n("invalid_number_min", min=self.min),
+ )
+
+ if self.max is not None and int(self.value) > self.max:
+ raise YunohostValidationError(
+ "app_argument_invalid",
+ name=self.name,
+ error=m18n.n("invalid_number_max", max=self.max),
+ )
+
+
+class DisplayTextQuestion(Question):
+ argument_type = "display_text"
+ readonly = True
+
+ def __init__(self, question, context: Mapping[str, Any] = {}):
+ super().__init__(question, context)
+
+ self.optional = True
+ self.style = question.get(
+ "style", "info" if question["type"] == "alert" else ""
+ )
+
+ def _format_text_for_user_input_in_cli(self):
+ text = _value_for_locale(self.ask)
+
+ if self.style in ["success", "info", "warning", "danger"]:
+ color = {
+ "success": "green",
+ "info": "cyan",
+ "warning": "yellow",
+ "danger": "red",
+ }
+ prompt = m18n.g(self.style) if self.style != "danger" else m18n.n("danger")
+ return colorize(prompt, color[self.style]) + f" {text}"
+ else:
+ return text
+
+
+class FileQuestion(Question):
+ argument_type = "file"
+ upload_dirs: List[str] = []
+
+ @classmethod
+ def clean_upload_dirs(cls):
+ # Delete files uploaded from API
+ for upload_dir in cls.upload_dirs:
+ if os.path.exists(upload_dir):
+ shutil.rmtree(upload_dir)
+
+ def __init__(self, question, context: Mapping[str, Any] = {}):
+ super().__init__(question, context)
+ self.accept = question.get("accept", "")
+
+ def _prevalidate(self):
+ if self.value is None:
+ self.value = self.current_value
+
+ super()._prevalidate()
+
+ if Moulinette.interface.type != "api":
+ if not self.value or not os.path.exists(str(self.value)):
+ raise YunohostValidationError(
+ "app_argument_invalid",
+ name=self.name,
+ error=m18n.n("file_does_not_exist", path=str(self.value)),
+ )
+
+ def _post_parse_value(self):
+ from base64 import b64decode
+
+ if not self.value:
+ return self.value
+
+ upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_")
+ _, file_path = tempfile.mkstemp(dir=upload_dir)
+
+ FileQuestion.upload_dirs += [upload_dir]
+
+ logger.debug(f"Saving file {self.name} for file question into {file_path}")
+
+ def is_file_path(s):
+ return isinstance(s, str) and s.startswith("/") and os.path.exists(s)
+
+ if Moulinette.interface.type != "api" or is_file_path(self.value):
+ content = read_file(str(self.value), file_mode="rb")
+ else:
+ content = b64decode(self.value)
+
+ write_to_file(file_path, content, file_mode="wb")
+
+ self.value = file_path
+
+ return self.value
+
+
+ARGUMENTS_TYPE_PARSERS = {
+ "string": StringQuestion,
+ "text": StringQuestion,
+ "select": StringQuestion,
+ "tags": TagsQuestion,
+ "email": EmailQuestion,
+ "url": URLQuestion,
+ "date": DateQuestion,
+ "time": TimeQuestion,
+ "color": ColorQuestion,
+ "password": PasswordQuestion,
+ "path": PathQuestion,
+ "boolean": BooleanQuestion,
+ "domain": DomainQuestion,
+ "user": UserQuestion,
+ "number": NumberQuestion,
+ "range": NumberQuestion,
+ "display_text": DisplayTextQuestion,
+ "alert": DisplayTextQuestion,
+ "markdown": DisplayTextQuestion,
+ "file": FileQuestion,
+}
+
+
+def ask_questions_and_parse_answers(
+ raw_questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}
+) -> List[Question]:
+ """Parse arguments store in either manifest.json or actions.json or from a
+ config panel against the user answers when they are present.
+
+ Keyword arguments:
+ raw_questions -- the arguments description store in yunohost
+ format from actions.json/toml, manifest.json/toml
+ or config_panel.json/toml
+ prefilled_answers -- a url "query-string" such as "domain=yolo.test&path=/foobar&admin=sam"
+ or a dict such as {"domain": "yolo.test", "path": "/foobar", "admin": "sam"}
+ """
+
+ if isinstance(prefilled_answers, str):
+ # FIXME FIXME : this is not uniform with config_set() which uses parse.qs (no l)
+ # parse_qsl parse single values
+ # whereas parse.qs return list of values (which is useful for tags, etc)
+ # For now, let's not migrate this piece of code to parse_qs
+ # Because Aleks believes some bits of the app CI rely on overriding values (e.g. foo=foo&...&foo=bar)
+ answers = dict(
+ urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True)
+ )
+ elif isinstance(prefilled_answers, Mapping):
+ answers = {**prefilled_answers}
+ else:
+ answers = {}
+
+ out = []
+
+ for raw_question in raw_questions:
+ question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]
+ raw_question["value"] = answers.get(raw_question["name"])
+ question = question_class(raw_question, context=answers)
+ answers[question.name] = question.ask_if_needed()
+ out.append(question)
+
+ return out
diff --git a/src/yunohost/utils/dns.py b/src/yunohost/utils/dns.py
new file mode 100644
index 000000000..ccb6c5406
--- /dev/null
+++ b/src/yunohost/utils/dns.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+
+""" License
+
+ Copyright (C) 2018 YUNOHOST.ORG
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses
+
+"""
+import dns.resolver
+from typing import List
+
+from moulinette.utils.filesystem import read_file
+
+SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"]
+
+YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"]
+
+# Lazy dev caching to avoid re-reading the file multiple time when calling
+# dig() often during same yunohost operation
+external_resolvers_: List[str] = []
+
+
+def is_yunohost_dyndns_domain(domain):
+
+ return any(
+ domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS
+ )
+
+
+def is_special_use_tld(domain):
+
+ return any(domain.endswith(f".{tld}") for tld in SPECIAL_USE_TLDS)
+
+
+def external_resolvers():
+
+ global external_resolvers_
+
+ if not external_resolvers_:
+ resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n")
+ external_resolvers_ = [
+ r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver")
+ ]
+ # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6
+ # will be tried anyway resulting in super-slow dig requests that'll wait
+ # until timeout...
+ external_resolvers_ = [r for r in external_resolvers_ if ":" not in r]
+
+ return external_resolvers_
+
+
+def dig(
+ qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False
+):
+ """
+ Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf
+ """
+
+ # It's very important to do the request with a qname ended by .
+ # If we don't and the domain fail, dns resolver try a second request
+ # by concatenate the qname with the end of the "hostname"
+ if not qname.endswith("."):
+ qname += "."
+
+ if resolvers == "local":
+ resolvers = ["127.0.0.1"]
+ elif resolvers == "force_external":
+ resolvers = external_resolvers()
+ else:
+ assert isinstance(resolvers, list)
+
+ resolver = dns.resolver.Resolver(configure=False)
+ resolver.use_edns(0, 0, edns_size)
+ resolver.nameservers = resolvers
+ # resolver.timeout is used to trigger the next DNS query on resolvers list.
+ # In python-dns 1.16, this value is set to 2.0. However, this means that if
+ # the 3 first dns resolvers in list are down, we wait 6 seconds before to
+ # run the DNS query to a DNS resolvers up...
+ # In diagnosis dnsrecords, with 10 domains this means at least 12min, too long.
+ resolver.timeout = 1.0
+ # resolver.lifetime is the timeout for resolver.query()
+ # By default set it to 5 seconds to allow 4 resolvers to be unreachable.
+ resolver.lifetime = timeout
+ try:
+ answers = resolver.query(qname, rdtype)
+ except (
+ dns.resolver.NXDOMAIN,
+ dns.resolver.NoNameservers,
+ dns.resolver.NoAnswer,
+ dns.exception.Timeout,
+ ) as e:
+ return ("nok", (e.__class__.__name__, e))
+
+ if not full_answers:
+ answers = [answer.to_text() for answer in answers]
+
+ return ("ok", answers)
diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py
index 3000a52f8..8405830e7 100644
--- a/src/yunohost/utils/error.py
+++ b/src/yunohost/utils/error.py
@@ -25,6 +25,8 @@ from moulinette import m18n
class YunohostError(MoulinetteError):
+ http_code = 500
+
"""
Yunohost base exception
@@ -46,6 +48,15 @@ class YunohostError(MoulinetteError):
def content(self):
if not self.log_ref:
- return super(YunohostError, self).content()
+ return super().content()
else:
return {"error": self.strerror, "log_ref": self.log_ref}
+
+
+class YunohostValidationError(YunohostError):
+
+ http_code = 400
+
+ def content(self):
+
+ return {"error": self.strerror, "error_key": self.key, **self.kwargs}
diff --git a/src/yunohost/utils/i18n.py b/src/yunohost/utils/i18n.py
new file mode 100644
index 000000000..a0daf8181
--- /dev/null
+++ b/src/yunohost/utils/i18n.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+
+""" License
+
+ Copyright (C) 2018 YUNOHOST.ORG
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses
+
+"""
+from moulinette import m18n
+
+
+def _value_for_locale(values):
+ """
+ Return proper value for current locale
+
+ Keyword arguments:
+ values -- A dict of values associated to their locale
+
+ Returns:
+ An utf-8 encoded string
+
+ """
+ if not isinstance(values, dict):
+ return values
+
+ for lang in [m18n.locale, m18n.default_locale]:
+ try:
+ return values[lang]
+ except KeyError:
+ continue
+
+ # Fallback to first value
+ return list(values.values())[0]
diff --git a/src/yunohost/utils/ldap.py b/src/yunohost/utils/ldap.py
index 85bca34d7..651d09f75 100644
--- a/src/yunohost/utils/ldap.py
+++ b/src/yunohost/utils/ldap.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-
""" License
Copyright (C) 2019 YunoHost
@@ -21,10 +20,18 @@
import os
import atexit
-from moulinette.core import MoulinetteLdapIsDownError
-from moulinette.authenticators import ldap
+import logging
+import ldap
+import ldap.sasl
+import time
+import ldap.modlist as modlist
+
+from moulinette import m18n
+from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
+logger = logging.getLogger("yunohost.utils.ldap")
+
# We use a global variable to do some caching
# to avoid re-authenticating in case we call _get_ldap_authenticator multiple times
_ldap_interface = None
@@ -35,47 +42,17 @@ def _get_ldap_interface():
global _ldap_interface
if _ldap_interface is None:
-
- conf = {
- "vendor": "ldap",
- "name": "as-root",
- "parameters": {
- "uri": "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi",
- "base_dn": "dc=yunohost,dc=org",
- "user_rdn": "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth",
- },
- "extra": {},
- }
-
- try:
- _ldap_interface = ldap.Authenticator(**conf)
- except MoulinetteLdapIsDownError:
- raise YunohostError(
- "Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'"
- )
-
- assert_slapd_is_running()
+ _ldap_interface = LDAPInterface()
return _ldap_interface
-def assert_slapd_is_running():
-
- # Assert slapd is running...
- if not os.system("pgrep slapd >/dev/null") == 0:
- raise YunohostError(
- "Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'"
- )
-
-
# We regularly want to extract stuff like 'bar' in ldap path like
# foo=bar,dn=users.example.org,ou=example.org,dc=org so this small helper allow
# to do this without relying of dozens of mysterious string.split()[0]
#
# e.g. using _ldap_path_extract(path, "foo") on the previous example will
# return bar
-
-
def _ldap_path_extract(path, info):
for element in path.split(","):
if element.startswith(info + "="):
@@ -93,3 +70,247 @@ def _destroy_ldap_interface():
atexit.register(_destroy_ldap_interface)
+
+
+class LDAPInterface:
+ def __init__(self):
+ logger.debug("initializing ldap interface")
+
+ self.uri = "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi"
+ self.basedn = "dc=yunohost,dc=org"
+ self.rootdn = "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
+ self.connect()
+
+ def connect(self):
+ def _reconnect():
+ con = ldap.ldapobject.ReconnectLDAPObject(
+ self.uri, retry_max=10, retry_delay=0.5
+ )
+ con.sasl_non_interactive_bind_s("EXTERNAL")
+ return con
+
+ try:
+ con = _reconnect()
+ except ldap.SERVER_DOWN:
+ # ldap is down, attempt to restart it before really failing
+ logger.warning(m18n.n("ldap_server_is_down_restart_it"))
+ os.system("systemctl restart slapd")
+ time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted
+ try:
+ con = _reconnect()
+ except ldap.SERVER_DOWN:
+ raise YunohostError(
+ "Service slapd is not running but is required to perform this action ... "
+ "You can try to investigate what's happening with 'systemctl status slapd'",
+ raw_msg=True,
+ )
+
+ # Check that we are indeed logged in with the right identity
+ try:
+ # whoami_s return dn:..., then delete these 3 characters
+ who = con.whoami_s()[3:]
+ except Exception as e:
+ logger.warning("Error during ldap authentication process: %s", e)
+ raise
+ else:
+ if who != self.rootdn:
+ raise MoulinetteError("Not logged in with the expected userdn ?!")
+ else:
+ self.con = con
+
+ def __del__(self):
+ """Disconnect and free ressources"""
+ if hasattr(self, "con") and self.con:
+ self.con.unbind_s()
+
+ def search(self, base=None, filter="(objectClass=*)", attrs=["dn"]):
+ """Search in LDAP base
+
+ Perform an LDAP search operation with given arguments and return
+ results as a list.
+
+ Keyword arguments:
+ - base -- The dn to search into
+ - filter -- A string representation of the filter to apply
+ - attrs -- A list of attributes to fetch
+
+ Returns:
+ A list of all results
+
+ """
+ if not base:
+ base = self.basedn
+
+ try:
+ result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
+ except Exception as e:
+ raise MoulinetteError(
+ "error during LDAP search operation with: base='%s', "
+ "filter='%s', attrs=%s and exception %s" % (base, filter, attrs, e),
+ raw_msg=True,
+ )
+
+ result_list = []
+ if not attrs or "dn" not in attrs:
+ result_list = [entry for dn, entry in result]
+ else:
+ for dn, entry in result:
+ entry["dn"] = [dn]
+ result_list.append(entry)
+
+ def decode(value):
+ if isinstance(value, bytes):
+ value = value.decode("utf-8")
+ return value
+
+ # result_list is for example :
+ # [{'virtualdomain': [b'test.com']}, {'virtualdomain': [b'yolo.test']},
+ for stuff in result_list:
+ if isinstance(stuff, dict):
+ for key, values in stuff.items():
+ stuff[key] = [decode(v) for v in values]
+
+ return result_list
+
+ def add(self, rdn, attr_dict):
+ """
+ Add LDAP entry
+
+ Keyword arguments:
+ rdn -- DN without domain
+ attr_dict -- Dictionnary of attributes/values to add
+
+ Returns:
+ Boolean | MoulinetteError
+
+ """
+ dn = rdn + "," + self.basedn
+ ldif = modlist.addModlist(attr_dict)
+ for i, (k, v) in enumerate(ldif):
+ if isinstance(v, list):
+ v = [a.encode("utf-8") for a in v]
+ elif isinstance(v, str):
+ v = [v.encode("utf-8")]
+ ldif[i] = (k, v)
+
+ try:
+ self.con.add_s(dn, ldif)
+ except Exception as e:
+ raise MoulinetteError(
+ "error during LDAP add operation with: rdn='%s', "
+ "attr_dict=%s and exception %s" % (rdn, attr_dict, e),
+ raw_msg=True,
+ )
+ else:
+ return True
+
+ def remove(self, rdn):
+ """
+ Remove LDAP entry
+
+ Keyword arguments:
+ rdn -- DN without domain
+
+ Returns:
+ Boolean | MoulinetteError
+
+ """
+ dn = rdn + "," + self.basedn
+ try:
+ self.con.delete_s(dn)
+ except Exception as e:
+ raise MoulinetteError(
+ "error during LDAP delete operation with: rdn='%s' and exception %s"
+ % (rdn, e),
+ raw_msg=True,
+ )
+ else:
+ return True
+
+ def update(self, rdn, attr_dict, new_rdn=False):
+ """
+ Modify LDAP entry
+
+ Keyword arguments:
+ rdn -- DN without domain
+ attr_dict -- Dictionnary of attributes/values to add
+ new_rdn -- New RDN for modification
+
+ Returns:
+ Boolean | MoulinetteError
+
+ """
+ dn = rdn + "," + self.basedn
+ actual_entry = self.search(base=dn, attrs=None)
+ ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1)
+
+ if ldif == []:
+ logger.debug("Nothing to update in LDAP")
+ return True
+
+ try:
+ if new_rdn:
+ self.con.rename_s(dn, new_rdn)
+ new_base = dn.split(",", 1)[1]
+ dn = new_rdn + "," + new_base
+
+ for i, (a, k, vs) in enumerate(ldif):
+ if isinstance(vs, list):
+ vs = [v.encode("utf-8") for v in vs]
+ elif isinstance(vs, str):
+ vs = [vs.encode("utf-8")]
+ ldif[i] = (a, k, vs)
+
+ self.con.modify_ext_s(dn, ldif)
+ except Exception as e:
+ raise MoulinetteError(
+ "error during LDAP update operation with: rdn='%s', "
+ "attr_dict=%s, new_rdn=%s and exception: %s"
+ % (rdn, attr_dict, new_rdn, e),
+ raw_msg=True,
+ )
+ else:
+ return True
+
+ def validate_uniqueness(self, value_dict):
+ """
+ Check uniqueness of values
+
+ Keyword arguments:
+ value_dict -- Dictionnary of attributes/values to check
+
+ Returns:
+ Boolean | MoulinetteError
+
+ """
+ attr_found = self.get_conflict(value_dict)
+ if attr_found:
+ logger.info(
+ "attribute '%s' with value '%s' is not unique",
+ attr_found[0],
+ attr_found[1],
+ )
+ raise YunohostError(
+ "ldap_attribute_already_exists",
+ attribute=attr_found[0],
+ value=attr_found[1],
+ )
+ return True
+
+ def get_conflict(self, value_dict, base_dn=None):
+ """
+ Check uniqueness of values
+
+ Keyword arguments:
+ value_dict -- Dictionnary of attributes/values to check
+
+ Returns:
+ None | tuple with Fist conflict attribute name and value
+
+ """
+ for attr, value in value_dict.items():
+ if not self.search(base=base_dn, filter=attr + "=" + value):
+ continue
+ else:
+ return (attr, value)
+ return None
diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py
index c84817f98..87c163f1b 100644
--- a/src/yunohost/utils/legacy.py
+++ b/src/yunohost/utils/legacy.py
@@ -1,168 +1,33 @@
import os
+import re
+import glob
from moulinette import m18n
-from yunohost.utils.error import YunohostError
+from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
-from moulinette.utils.filesystem import write_to_json, read_yaml
+from moulinette.utils.filesystem import (
+ read_file,
+ write_to_file,
+ write_to_json,
+ write_to_yaml,
+ read_yaml,
+)
-from yunohost.user import user_list, user_group_create, user_group_update
+from yunohost.user import user_list
from yunohost.app import (
- app_setting,
_installed_apps,
_get_app_settings,
_set_app_settings,
)
from yunohost.permission import (
permission_create,
- user_permission_list,
user_permission_update,
permission_sync_to_user,
)
+from yunohost.utils.error import YunohostValidationError
+
logger = getActionLogger("yunohost.legacy")
-
-class SetupGroupPermissions:
- @staticmethod
- def remove_if_exists(target):
-
- from yunohost.utils.ldap import _get_ldap_interface
-
- ldap = _get_ldap_interface()
-
- try:
- objects = ldap.search(target + ",dc=yunohost,dc=org")
- # ldap search will raise an exception if no corresponding object is found >.> ...
- except Exception:
- logger.debug("%s does not exist, no need to delete it" % target)
- return
-
- objects.reverse()
- for o in objects:
- for dn in o["dn"]:
- dn = dn.replace(",dc=yunohost,dc=org", "")
- logger.debug("Deleting old object %s ..." % dn)
- try:
- ldap.remove(dn)
- except Exception as e:
- raise YunohostError(
- "migration_0011_failed_to_remove_stale_object", dn=dn, error=e
- )
-
- @staticmethod
- def migrate_LDAP_db():
-
- logger.info(m18n.n("migration_0011_update_LDAP_database"))
-
- from yunohost.utils.ldap import _get_ldap_interface
-
- ldap = _get_ldap_interface()
-
- ldap_map = read_yaml(
- "/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml"
- )
-
- try:
- SetupGroupPermissions.remove_if_exists("ou=permission")
- SetupGroupPermissions.remove_if_exists("ou=groups")
-
- attr_dict = ldap_map["parents"]["ou=permission"]
- ldap.add("ou=permission", attr_dict)
-
- attr_dict = ldap_map["parents"]["ou=groups"]
- ldap.add("ou=groups", attr_dict)
-
- attr_dict = ldap_map["children"]["cn=all_users,ou=groups"]
- ldap.add("cn=all_users,ou=groups", attr_dict)
-
- attr_dict = ldap_map["children"]["cn=visitors,ou=groups"]
- ldap.add("cn=visitors,ou=groups", attr_dict)
-
- for rdn, attr_dict in ldap_map["depends_children"].items():
- ldap.add(rdn, attr_dict)
- except Exception as e:
- raise YunohostError("migration_0011_LDAP_update_failed", error=e)
-
- logger.info(m18n.n("migration_0011_create_group"))
-
- # Create a group for each yunohost user
- user_list = ldap.search(
- "ou=users,dc=yunohost,dc=org",
- "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))",
- ["uid", "uidNumber"],
- )
- for user_info in user_list:
- username = user_info["uid"][0]
- ldap.update(
- "uid=%s,ou=users" % username,
- {
- "objectClass": [
- "mailAccount",
- "inetOrgPerson",
- "posixAccount",
- "userPermissionYnh",
- ]
- },
- )
- user_group_create(
- username,
- gid=user_info["uidNumber"][0],
- primary_group=True,
- sync_perm=False,
- )
- user_group_update(
- groupname="all_users", add=username, force=True, sync_perm=False
- )
-
- @staticmethod
- def migrate_app_permission(app=None):
- logger.info(m18n.n("migration_0011_migrate_permission"))
-
- apps = _installed_apps()
-
- if app:
- if app not in apps:
- logger.error(
- "Can't migrate permission for app %s because it ain't installed..."
- % app
- )
- apps = []
- else:
- apps = [app]
-
- for app in apps:
- permission = app_setting(app, "allowed_users")
- path = app_setting(app, "path")
- domain = app_setting(app, "domain")
-
- url = "/" if domain and path else None
- if permission:
- known_users = list(user_list()["users"].keys())
- allowed = [
- user for user in permission.split(",") if user in known_users
- ]
- else:
- allowed = ["all_users"]
- permission_create(
- app + ".main",
- url=url,
- allowed=allowed,
- show_tile=True,
- protected=False,
- sync_perm=False,
- )
-
- app_setting(app, "allowed_users", delete=True)
-
- # Migrate classic public app still using the legacy unprotected_uris
- if (
- app_setting(app, "unprotected_uris") == "/"
- or app_setting(app, "skipped_uris") == "/"
- ):
- user_permission_update(app + ".main", add="visitors", sync_perm=False)
-
- permission_sync_to_user()
-
-
LEGACY_PERMISSION_LABEL = {
("nextcloud", "skipped"): "api", # .well-known
("libreto", "skipped"): "pad access", # /[^/]+
@@ -280,7 +145,7 @@ def migrate_legacy_permission_settings(app=None):
auth_header=True,
label=legacy_permission_label(app, "protected"),
show_tile=False,
- allowed=user_permission_list()["permissions"][app + ".main"]["allowed"],
+ allowed=[],
protected=True,
sync_perm=False,
)
@@ -308,6 +173,9 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent():
if not os.path.exists(persistent_file_name):
return
+ # Ugly hack because for some reason so many people have tabs in their conf.json.persistent ...
+ os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent")
+
# Ugly hack to try not to misarably fail migration
persistent = read_yaml(persistent_file_name)
@@ -378,5 +246,215 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent():
write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4)
logger.warning(
- "Yunohost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system"
+ "YunoHost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system"
)
+
+
+LEGACY_PHP_VERSION_REPLACEMENTS = [
+ ("/etc/php5", "/etc/php/7.3"),
+ ("/etc/php/7.0", "/etc/php/7.3"),
+ ("/var/run/php5-fpm", "/var/run/php/php7.3-fpm"),
+ ("/var/run/php/php7.0-fpm", "/var/run/php/php7.3-fpm"),
+ ("php5", "php7.3"),
+ ("php7.0", "php7.3"),
+ (
+ 'phpversion="${phpversion:-7.0}"',
+ 'phpversion="${phpversion:-7.3}"',
+ ), # Many helpers like the composer ones use 7.0 by default ...
+ (
+ '"$phpversion" == "7.0"',
+ '$(bc <<< "$phpversion >= 7.3") -eq 1',
+ ), # patch ynh_install_php to refuse installing/removing php <= 7.3
+]
+
+
+def _patch_legacy_php_versions(app_folder):
+
+ files_to_patch = []
+ files_to_patch.extend(glob.glob("%s/conf/*" % app_folder))
+ files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder))
+ files_to_patch.extend(glob.glob("%s/scripts/*/*" % app_folder))
+ files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder))
+ files_to_patch.append("%s/manifest.json" % app_folder)
+ files_to_patch.append("%s/manifest.toml" % app_folder)
+
+ for filename in files_to_patch:
+
+ # Ignore non-regular files
+ if not os.path.isfile(filename):
+ continue
+
+ c = (
+ "sed -i "
+ + "".join(
+ "-e 's@{pattern}@{replace}@g' ".format(pattern=p, replace=r)
+ for p, r in LEGACY_PHP_VERSION_REPLACEMENTS
+ )
+ + "%s" % filename
+ )
+ os.system(c)
+
+
+def _patch_legacy_php_versions_in_settings(app_folder):
+
+ settings = read_yaml(os.path.join(app_folder, "settings.yml"))
+
+ if settings.get("fpm_config_dir") == "/etc/php/7.0/fpm":
+ settings["fpm_config_dir"] = "/etc/php/7.3/fpm"
+ if settings.get("fpm_service") == "php7.0-fpm":
+ settings["fpm_service"] = "php7.3-fpm"
+ if settings.get("phpversion") == "7.0":
+ settings["phpversion"] = "7.3"
+
+ # We delete these checksums otherwise the file will appear as manually modified
+ list_to_remove = ["checksum__etc_php_7.0_fpm_pool", "checksum__etc_nginx_conf.d"]
+ settings = {
+ k: v
+ for k, v in settings.items()
+ if not any(k.startswith(to_remove) for to_remove in list_to_remove)
+ }
+
+ write_to_yaml(app_folder + "/settings.yml", settings)
+
+
+def _patch_legacy_helpers(app_folder):
+
+ files_to_patch = []
+ files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder))
+ files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder))
+
+ stuff_to_replace = {
+ # Replace
+ # sudo yunohost app initdb $db_user -p $db_pwd
+ # by
+ # ynh_mysql_setup_db --db_user=$db_user --db_name=$db_user --db_pwd=$db_pwd
+ "yunohost app initdb": {
+ "pattern": r"(sudo )?yunohost app initdb \"?(\$\{?\w+\}?)\"?\s+-p\s\"?(\$\{?\w+\}?)\"?",
+ "replace": r"ynh_mysql_setup_db --db_user=\2 --db_name=\2 --db_pwd=\3",
+ "important": True,
+ },
+ # Replace
+ # sudo yunohost app checkport whaterver
+ # by
+ # ynh_port_available whatever
+ "yunohost app checkport": {
+ "pattern": r"(sudo )?yunohost app checkport",
+ "replace": r"ynh_port_available",
+ "important": True,
+ },
+ # We can't migrate easily port-available
+ # .. but at the time of writing this code, only two non-working apps are using it.
+ "yunohost tools port-available": {"important": True},
+ # Replace
+ # yunohost app checkurl "${domain}${path_url}" -a "${app}"
+ # by
+ # ynh_webpath_register --app=${app} --domain=${domain} --path_url=${path_url}
+ "yunohost app checkurl": {
+ "pattern": r"(sudo )?yunohost app checkurl \"?(\$\{?\w+\}?)\/?(\$\{?\w+\}?)\"?\s+-a\s\"?(\$\{?\w+\}?)\"?",
+ "replace": r"ynh_webpath_register --app=\4 --domain=\2 --path_url=\3",
+ "important": True,
+ },
+ # Remove
+ # Automatic diagnosis data from YunoHost
+ # __PRE_TAG1__$(yunohost tools diagnosis | ...)__PRE_TAG2__"
+ #
+ "yunohost tools diagnosis": {
+ "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?",
+ "replace": r"",
+ "important": False,
+ },
+ # Old $1, $2 in backup/restore scripts...
+ "app=$2": {
+ "only_for": ["scripts/backup", "scripts/restore"],
+ "pattern": r"app=\$2",
+ "replace": r"app=$YNH_APP_INSTANCE_NAME",
+ "important": True,
+ },
+ # Old $1, $2 in backup/restore scripts...
+ "backup_dir=$1": {
+ "only_for": ["scripts/backup", "scripts/restore"],
+ "pattern": r"backup_dir=\$1",
+ "replace": r"backup_dir=.",
+ "important": True,
+ },
+ # Old $1, $2 in backup/restore scripts...
+ "restore_dir=$1": {
+ "only_for": ["scripts/restore"],
+ "pattern": r"restore_dir=\$1",
+ "replace": r"restore_dir=.",
+ "important": True,
+ },
+ # Old $1, $2 in install scripts...
+ # We ain't patching that shit because it ain't trivial to patch all args...
+ "domain=$1": {"only_for": ["scripts/install"], "important": True},
+ }
+
+ for helper, infos in stuff_to_replace.items():
+ infos["pattern"] = (
+ re.compile(infos["pattern"]) if infos.get("pattern") else None
+ )
+ infos["replace"] = infos.get("replace")
+
+ for filename in files_to_patch:
+
+ # Ignore non-regular files
+ if not os.path.isfile(filename):
+ continue
+
+ try:
+ content = read_file(filename)
+ except MoulinetteError:
+ continue
+
+ replaced_stuff = False
+ show_warning = False
+
+ for helper, infos in stuff_to_replace.items():
+
+ # Ignore if not relevant for this file
+ if infos.get("only_for") and not any(
+ filename.endswith(f) for f in infos["only_for"]
+ ):
+ continue
+
+ # If helper is used, attempt to patch the file
+ if helper in content and infos["pattern"]:
+ content = infos["pattern"].sub(infos["replace"], content)
+ replaced_stuff = True
+ if infos["important"]:
+ show_warning = True
+
+ # If the helper is *still* in the content, it means that we
+ # couldn't patch the deprecated helper in the previous lines. In
+ # that case, abort the install or whichever step is performed
+ if helper in content and infos["important"]:
+ raise YunohostValidationError(
+ "This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.",
+ raw_msg=True,
+ )
+
+ if replaced_stuff:
+
+ # Check the app do load the helper
+ # If it doesn't, add the instruction ourselve (making sure it's after the #!/bin/bash if it's there...
+ if filename.split("/")[-1] in [
+ "install",
+ "remove",
+ "upgrade",
+ "backup",
+ "restore",
+ ]:
+ source_helpers = "source /usr/share/yunohost/helpers"
+ if source_helpers not in content:
+ content.replace("#!/bin/bash", "#!/bin/bash\n" + source_helpers)
+ if source_helpers not in content:
+ content = source_helpers + "\n" + content
+
+ # Actually write the new content in the file
+ write_to_file(filename, content)
+
+ if show_warning:
+ # And complain about those damn deprecated helpers
+ logger.error(
+ r"/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ..."
+ )
diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py
index d96151fa4..4474af14f 100644
--- a/src/yunohost/utils/network.py
+++ b/src/yunohost/utils/network.py
@@ -22,7 +22,6 @@ import os
import re
import logging
import time
-import dns.resolver
from moulinette.utils.filesystem import read_file, write_to_file
from moulinette.utils.network import download_text
@@ -124,68 +123,6 @@ def get_gateway():
return addr.popitem()[1] if len(addr) == 1 else None
-# Lazy dev caching to avoid re-reading the file multiple time when calling
-# dig() often during same yunohost operation
-external_resolvers_ = []
-
-
-def external_resolvers():
-
- global external_resolvers_
-
- if not external_resolvers_:
- resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n")
- external_resolvers_ = [
- r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver")
- ]
- # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6
- # will be tried anyway resulting in super-slow dig requests that'll wait
- # until timeout...
- external_resolvers_ = [r for r in external_resolvers_ if ":" not in r]
-
- return external_resolvers_
-
-
-def dig(
- qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False
-):
- """
- Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf
- """
-
- # It's very important to do the request with a qname ended by .
- # If we don't and the domain fail, dns resolver try a second request
- # by concatenate the qname with the end of the "hostname"
- if not qname.endswith("."):
- qname += "."
-
- if resolvers == "local":
- resolvers = ["127.0.0.1"]
- elif resolvers == "force_external":
- resolvers = external_resolvers()
- else:
- assert isinstance(resolvers, list)
-
- resolver = dns.resolver.Resolver(configure=False)
- resolver.use_edns(0, 0, edns_size)
- resolver.nameservers = resolvers
- resolver.timeout = timeout
- try:
- answers = resolver.query(qname, rdtype)
- except (
- dns.resolver.NXDOMAIN,
- dns.resolver.NoNameservers,
- dns.resolver.NoAnswer,
- dns.exception.Timeout,
- ) as e:
- return ("nok", (e.__class__.__name__, e))
-
- if not full_answers:
- answers = [answer.to_text() for answer in answers]
-
- return ("ok", answers)
-
-
def _extract_inet(string, skip_netmask=False, skip_loopback=True):
"""
Extract IP addresses (v4 and/or v6) from a string limited to one
diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py
index dce337f84..188850183 100644
--- a/src/yunohost/utils/password.py
+++ b/src/yunohost/utils/password.py
@@ -90,11 +90,11 @@ class PasswordValidator(object):
# on top (at least not the moulinette ones)
# because the moulinette needs to be correctly initialized
# as well as modules available in python's path.
- from yunohost.utils.error import YunohostError
+ from yunohost.utils.error import YunohostValidationError
status, msg = self.validation_summary(password)
if status == "error":
- raise YunohostError(msg)
+ raise YunohostValidationError(msg)
def validation_summary(self, password):
"""
@@ -111,8 +111,13 @@ class PasswordValidator(object):
listed = password in SMALL_PWD_LIST or self.is_in_most_used_list(password)
strength_level = self.strength_level(password)
if listed:
+ # i18n: password_listed
return ("error", "password_listed")
if strength_level < self.validation_strength:
+ # i18n: password_too_simple_1
+ # i18n: password_too_simple_2
+ # i18n: password_too_simple_3
+ # i18n: password_too_simple_4
return ("error", "password_too_simple_%s" % self.validation_strength)
return ("success", "")
diff --git a/tests/add_missing_keys.py b/tests/add_missing_keys.py
new file mode 100644
index 000000000..30c6c6640
--- /dev/null
+++ b/tests/add_missing_keys.py
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+
+import os
+import re
+import glob
+import json
+import yaml
+import subprocess
+
+###############################################################################
+# Find used keys in python code #
+###############################################################################
+
+
+def find_expected_string_keys():
+
+ # Try to find :
+ # m18n.n( "foo"
+ # YunohostError("foo"
+ # YunohostValidationError("foo"
+ # # i18n: foo
+ p1 = re.compile(r"m18n\.n\(\n*\s*[\"\'](\w+)[\"\']")
+ p2 = re.compile(r"YunohostError\(\n*\s*[\'\"](\w+)[\'\"]")
+ p3 = re.compile(r"YunohostValidationError\(\n*\s*[\'\"](\w+)[\'\"]")
+ p4 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?")
+
+ python_files = glob.glob("src/yunohost/*.py")
+ python_files.extend(glob.glob("src/yunohost/utils/*.py"))
+ python_files.extend(glob.glob("src/yunohost/data_migrations/*.py"))
+ python_files.extend(glob.glob("src/yunohost/authenticators/*.py"))
+ python_files.extend(glob.glob("data/hooks/diagnosis/*.py"))
+ python_files.append("bin/yunohost")
+
+ for python_file in python_files:
+ content = open(python_file).read()
+ for m in p1.findall(content):
+ if m.endswith("_"):
+ continue
+ yield m
+ for m in p2.findall(content):
+ if m.endswith("_"):
+ continue
+ yield m
+ for m in p3.findall(content):
+ if m.endswith("_"):
+ continue
+ yield m
+ for m in p4.findall(content):
+ yield m
+
+ # For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries)
+ # Also we expect to have "diagnosis_description_" for each diagnosis
+ p3 = re.compile(r"[\"\'](diagnosis_[a-z]+_\w+)[\"\']")
+ for python_file in glob.glob("data/hooks/diagnosis/*.py"):
+ content = open(python_file).read()
+ for m in p3.findall(content):
+ if m.endswith("_"):
+ # Ignore some name fragments which are actually concatenated with other stuff..
+ continue
+ yield m
+ yield "diagnosis_description_" + os.path.basename(python_file)[:-3].split("-")[
+ -1
+ ]
+
+ # For each migration, expect to find "migration_description_"
+ for path in glob.glob("src/yunohost/data_migrations/*.py"):
+ if "__init__" in path:
+ continue
+ yield "migration_description_" + os.path.basename(path)[:-3]
+
+ # For each default service, expect to find "service_description_"
+ for service, info in yaml.safe_load(
+ open("data/templates/yunohost/services.yml")
+ ).items():
+ if info is None:
+ continue
+ yield "service_description_" + service
+
+ # For all unit operations, expect to find "log_"
+ # A unit operation is created either using the @is_unit_operation decorator
+ # or using OperationLogger(
+ cmd = "grep -hr '@is_unit_operation' src/yunohost/ -A3 2>/dev/null | grep '^def' | sed -E 's@^def (\\w+)\\(.*@\\1@g'"
+ for funcname in (
+ subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n")
+ ):
+ yield "log_" + funcname
+
+ p4 = re.compile(r"OperationLogger\(\n*\s*[\"\'](\w+)[\"\']")
+ for python_file in python_files:
+ content = open(python_file).read()
+ for m in ("log_" + match for match in p4.findall(content)):
+ yield m
+
+ # Global settings descriptions
+ # Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ...
+ p5 = re.compile(r" \(\n*\s*[\"\'](\w[\w\.]+)[\"\'],")
+ content = open("src/yunohost/settings.py").read()
+ for m in (
+ "global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content)
+ ):
+ yield m
+
+ # Keys for the actionmap ...
+ for category in yaml.safe_load(open("data/actionsmap/yunohost.yml")).values():
+ if "actions" not in category.keys():
+ continue
+ for action in category["actions"].values():
+ if "arguments" not in action.keys():
+ continue
+ for argument in action["arguments"].values():
+ extra = argument.get("extra")
+ if not extra:
+ continue
+ if "password" in extra:
+ yield extra["password"]
+ if "ask" in extra:
+ yield extra["ask"]
+ if "comment" in extra:
+ yield extra["comment"]
+ if "pattern" in extra:
+ yield extra["pattern"][1]
+ if "help" in extra:
+ yield extra["help"]
+
+ # Hardcoded expected keys ...
+ yield "admin_password" # Not sure that's actually used nowadays...
+
+ for method in ["tar", "copy", "custom"]:
+ yield "backup_applying_method_%s" % method
+ yield "backup_method_%s_finished" % method
+
+ for level in ["danger", "thirdparty", "warning"]:
+ yield "confirm_app_install_%s" % level
+
+ for errortype in ["not_found", "error", "warning", "success", "not_found_details"]:
+ yield "diagnosis_domain_expiration_%s" % errortype
+ yield "diagnosis_domain_not_found_details"
+
+ for errortype in ["bad_status_code", "connection_error", "timeout"]:
+ yield "diagnosis_http_%s" % errortype
+
+ yield "password_listed"
+ for i in [1, 2, 3, 4]:
+ yield "password_too_simple_%s" % i
+
+ checks = [
+ "outgoing_port_25_ok",
+ "ehlo_ok",
+ "fcrdns_ok",
+ "blacklist_ok",
+ "queue_ok",
+ "ehlo_bad_answer",
+ "ehlo_unreachable",
+ "ehlo_bad_answer_details",
+ "ehlo_unreachable_details",
+ ]
+ for check in checks:
+ yield "diagnosis_mail_%s" % check
+
+
+###############################################################################
+# Load en locale json keys #
+###############################################################################
+
+
+def keys_defined_for_en():
+ return json.loads(open("locales/en.json").read()).keys()
+
+
+###############################################################################
+# Compare keys used and keys defined #
+###############################################################################
+
+
+expected_string_keys = set(find_expected_string_keys())
+keys_defined = set(keys_defined_for_en())
+
+
+undefined_keys = expected_string_keys.difference(keys_defined)
+undefined_keys = sorted(undefined_keys)
+
+
+j = json.loads(open("locales/en.json").read())
+for key in undefined_keys:
+ j[key] = "FIXME"
+
+json.dump(
+ j,
+ open("locales/en.json", "w"),
+ indent=4,
+ ensure_ascii=False,
+ sort_keys=True,
+)
diff --git a/tests/autofix_locale_format.py b/tests/autofix_locale_format.py
new file mode 100644
index 000000000..f3825bd30
--- /dev/null
+++ b/tests/autofix_locale_format.py
@@ -0,0 +1,53 @@
+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)
diff --git a/tests/reformat_locales.py b/tests/reformat_locales.py
new file mode 100644
index 000000000..86c2664d7
--- /dev/null
+++ b/tests/reformat_locales.py
@@ -0,0 +1,60 @@
+import re
+
+
+def reformat(lang, transformations):
+
+ locale = open(f"../locales/{lang}.json").read()
+ for pattern, replace in transformations.items():
+ locale = re.compile(pattern).sub(replace, locale)
+
+ open(f"../locales/{lang}.json", "w").write(locale)
+
+
+######################################################
+
+godamn_spaces_of_hell = [
+ "\u00a0",
+ "\u2000",
+ "\u2001",
+ "\u2002",
+ "\u2003",
+ "\u2004",
+ "\u2005",
+ "\u2006",
+ "\u2007",
+ "\u2008",
+ "\u2009",
+ "\u200A",
+ "\u202f",
+ "\u202F",
+ "\u3000",
+]
+
+transformations = {s: " " for s in godamn_spaces_of_hell}
+transformations.update(
+ {
+ "…": "...",
+ }
+)
+
+
+reformat("en", transformations)
+
+######################################################
+
+transformations.update(
+ {
+ "courriel": "email",
+ "e-mail": "email",
+ "Courriel": "Email",
+ "E-mail": "Email",
+ "« ": "'",
+ "«": "'",
+ " »": "'",
+ "»": "'",
+ "’": "'",
+ # r"$(\w{1,2})'|( \w{1,2})'": r"\1\2’",
+ }
+)
+
+reformat("fr", transformations)
diff --git a/tests/test_actionmap.py b/tests/test_actionmap.py
index bf6755979..0b8abb152 100644
--- a/tests/test_actionmap.py
+++ b/tests/test_actionmap.py
@@ -2,4 +2,4 @@ import yaml
def test_yaml_syntax():
- yaml.load(open("data/actionsmap/yunohost.yml"))
+ yaml.safe_load(open("data/actionsmap/yunohost.yml"))
diff --git a/tests/test_helpers.d/ynhtest_config.sh b/tests/test_helpers.d/ynhtest_config.sh
new file mode 100644
index 000000000..b64943a48
--- /dev/null
+++ b/tests/test_helpers.d/ynhtest_config.sh
@@ -0,0 +1,662 @@
+
+#################
+# _ __ _ _ #
+# | '_ \| | | | #
+# | |_) | |_| | #
+# | .__/ \__, | #
+# | | __/ | #
+# |_| |___/ #
+# #
+#################
+
+_read_py() {
+ local file="$1"
+ local key="$2"
+ python3 -c "exec(open('$file').read()); print($key)"
+}
+
+ynhtest_config_read_py() {
+
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.py"
+
+ cat << EOF > $dummy_dir/dummy.py
+# Some comment
+FOO = None
+ENABLED = False
+# TITLE = "Old title"
+TITLE = "Lorem Ipsum"
+THEME = "colib'ris"
+EMAIL = "root@example.com" # This is a comment without quotes
+PORT = 1234 # This is a comment without quotes
+URL = 'https://yunohost.org'
+DICT = {}
+DICT['ldap_base'] = "ou=users,dc=yunohost,dc=org"
+DICT['ldap_conf'] = {}
+DICT['ldap_conf']['user'] = "camille"
+# YNH_ICI
+DICT['TITLE'] = "Hello world"
+EOF
+
+ test "$(_read_py "$file" "FOO")" == "None"
+ test "$(ynh_read_var_in_file "$file" "FOO")" == "None"
+
+ test "$(_read_py "$file" "ENABLED")" == "False"
+ test "$(ynh_read_var_in_file "$file" "ENABLED")" == "False"
+
+ test "$(_read_py "$file" "TITLE")" == "Lorem Ipsum"
+ test "$(ynh_read_var_in_file "$file" "TITLE")" == "Lorem Ipsum"
+
+ test "$(_read_py "$file" "THEME")" == "colib'ris"
+ test "$(ynh_read_var_in_file "$file" "THEME")" == "colib'ris"
+
+ test "$(_read_py "$file" "EMAIL")" == "root@example.com"
+ test "$(ynh_read_var_in_file "$file" "EMAIL")" == "root@example.com"
+
+ test "$(_read_py "$file" "PORT")" == "1234"
+ test "$(ynh_read_var_in_file "$file" "PORT")" == "1234"
+
+ test "$(_read_py "$file" "URL")" == "https://yunohost.org"
+ test "$(ynh_read_var_in_file "$file" "URL")" == "https://yunohost.org"
+
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org"
+
+ test "$(ynh_read_var_in_file "$file" "user")" == "camille"
+
+ test "$(ynh_read_var_in_file "$file" "TITLE" "YNH_ICI")" == "Hello world"
+
+ ! _read_py "$file" "NONEXISTENT"
+ test "$(ynh_read_var_in_file "$file" "NONEXISTENT")" == "YNH_NULL"
+
+ ! _read_py "$file" "ENABLE"
+ test "$(ynh_read_var_in_file "$file" "ENABLE")" == "YNH_NULL"
+}
+
+ynhtest_config_write_py() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.py"
+
+ cat << EOF > $dummy_dir/dummy.py
+# Some comment
+FOO = None
+ENABLED = False
+# TITLE = "Old title"
+TITLE = "Lorem Ipsum"
+THEME = "colib'ris"
+EMAIL = "root@example.com" # This is a comment without quotes
+PORT = 1234 # This is a comment without quotes
+URL = 'https://yunohost.org'
+DICT = {}
+DICT['ldap_base'] = "ou=users,dc=yunohost,dc=org"
+# YNH_ICI
+DICT['TITLE'] = "Hello world"
+EOF
+
+ ynh_write_var_in_file "$file" "FOO" "bar"
+ test "$(_read_py "$file" "FOO")" == "bar"
+ test "$(ynh_read_var_in_file "$file" "FOO")" == "bar"
+
+ ynh_write_var_in_file "$file" "ENABLED" "True"
+ test "$(_read_py "$file" "ENABLED")" == "True"
+ test "$(ynh_read_var_in_file "$file" "ENABLED")" == "True"
+
+ ynh_write_var_in_file "$file" "TITLE" "Foo Bar"
+ test "$(_read_py "$file" "TITLE")" == "Foo Bar"
+ test "$(ynh_read_var_in_file "$file" "TITLE")" == "Foo Bar"
+
+ ynh_write_var_in_file "$file" "THEME" "super-awesome-theme"
+ test "$(_read_py "$file" "THEME")" == "super-awesome-theme"
+ test "$(ynh_read_var_in_file "$file" "THEME")" == "super-awesome-theme"
+
+ ynh_write_var_in_file "$file" "EMAIL" "sam@domain.tld"
+ test "$(_read_py "$file" "EMAIL")" == "sam@domain.tld"
+ test "$(ynh_read_var_in_file "$file" "EMAIL")" == "sam@domain.tld"
+
+ ynh_write_var_in_file "$file" "PORT" "5678"
+ test "$(_read_py "$file" "PORT")" == "5678"
+ test "$(ynh_read_var_in_file "$file" "PORT")" == "5678"
+
+ ynh_write_var_in_file "$file" "URL" "https://domain.tld/foobar"
+ test "$(_read_py "$file" "URL")" == "https://domain.tld/foobar"
+ test "$(ynh_read_var_in_file "$file" "URL")" == "https://domain.tld/foobar"
+
+ ynh_write_var_in_file "$file" "ldap_base" "ou=users,dc=yunohost,dc=org"
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org"
+
+ ynh_write_var_in_file "$file" "TITLE" "YOLO" "YNH_ICI"
+ test "$(ynh_read_var_in_file "$file" "TITLE" "YNH_ICI")" == "YOLO"
+
+ ! ynh_write_var_in_file "$file" "NONEXISTENT" "foobar"
+ ! _read_py "$file" "NONEXISTENT"
+ test "$(ynh_read_var_in_file "$file" "NONEXISTENT")" == "YNH_NULL"
+
+ ! ynh_write_var_in_file "$file" "ENABLE" "foobar"
+ ! _read_py "$file" "ENABLE"
+ test "$(ynh_read_var_in_file "$file" "ENABLE")" == "YNH_NULL"
+
+}
+
+###############
+# _ _ #
+# (_) (_) #
+# _ _ __ _ #
+# | | '_ \| | #
+# | | | | | | #
+# |_|_| |_|_| #
+# #
+###############
+
+_read_ini() {
+ local file="$1"
+ local key="$2"
+ python3 -c "import configparser; c = configparser.ConfigParser(); c.read('$file'); print(c['main']['$key'])"
+}
+
+ynhtest_config_read_ini() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.ini"
+
+ cat << EOF > $file
+# Some comment
+; Another comment
+[main]
+foo = null
+enabled = False
+# title = Old title
+title = Lorem Ipsum
+theme = colib'ris
+email = root@example.com ; This is a comment without quotes
+port = 1234 ; This is a comment without quotes
+url = https://yunohost.org
+[dict]
+ ldap_base = ou=users,dc=yunohost,dc=org
+EOF
+
+ test "$(_read_ini "$file" "foo")" == "null"
+ test "$(ynh_read_var_in_file "$file" "foo")" == "null"
+
+ test "$(_read_ini "$file" "enabled")" == "False"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "False"
+
+ test "$(_read_ini "$file" "title")" == "Lorem Ipsum"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum"
+
+ test "$(_read_ini "$file" "theme")" == "colib'ris"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris"
+
+ #test "$(_read_ini "$file" "email")" == "root@example.com"
+ test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com"
+
+ #test "$(_read_ini "$file" "port")" == "1234"
+ test "$(ynh_read_var_in_file "$file" "port")" == "1234"
+
+ test "$(_read_ini "$file" "url")" == "https://yunohost.org"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org"
+
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org"
+
+ ! _read_ini "$file" "nonexistent"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! _read_ini "$file" "enable"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+
+}
+
+ynhtest_config_write_ini() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.ini"
+
+ cat << EOF > $file
+# Some comment
+; Another comment
+[main]
+foo = null
+enabled = False
+# title = Old title
+title = Lorem Ipsum
+theme = colib'ris
+email = root@example.com # This is a comment without quotes
+port = 1234 # This is a comment without quotes
+url = https://yunohost.org
+[dict]
+ ldap_base = ou=users,dc=yunohost,dc=org
+EOF
+
+ ynh_write_var_in_file "$file" "foo" "bar"
+ test "$(_read_ini "$file" "foo")" == "bar"
+ test "$(ynh_read_var_in_file "$file" "foo")" == "bar"
+
+ ynh_write_var_in_file "$file" "enabled" "True"
+ test "$(_read_ini "$file" "enabled")" == "True"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "True"
+
+ ynh_write_var_in_file "$file" "title" "Foo Bar"
+ test "$(_read_ini "$file" "title")" == "Foo Bar"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar"
+
+ ynh_write_var_in_file "$file" "theme" "super-awesome-theme"
+ test "$(_read_ini "$file" "theme")" == "super-awesome-theme"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme"
+
+ ynh_write_var_in_file "$file" "email" "sam@domain.tld"
+ test "$(_read_ini "$file" "email")" == "sam@domain.tld # This is a comment without quotes"
+ test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld"
+
+ ynh_write_var_in_file "$file" "port" "5678"
+ test "$(_read_ini "$file" "port")" == "5678 # This is a comment without quotes"
+ test "$(ynh_read_var_in_file "$file" "port")" == "5678"
+
+ ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar"
+ test "$(_read_ini "$file" "url")" == "https://domain.tld/foobar"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar"
+
+ ynh_write_var_in_file "$file" "ldap_base" "ou=users,dc=yunohost,dc=org"
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org"
+
+ ! ynh_write_var_in_file "$file" "nonexistent" "foobar"
+ ! _read_ini "$file" "nonexistent"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! ynh_write_var_in_file "$file" "enable" "foobar"
+ ! _read_ini "$file" "enable"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+
+}
+
+#############################
+# _ #
+# | | #
+# _ _ __ _ _ __ ___ | | #
+# | | | |/ _` | '_ ` _ \| | #
+# | |_| | (_| | | | | | | | #
+# \__, |\__,_|_| |_| |_|_| #
+# __/ | #
+# |___/ #
+# #
+#############################
+
+_read_yaml() {
+ local file="$1"
+ local key="$2"
+ python3 -c "import yaml; print(yaml.safe_load(open('$file'))['$key'])"
+}
+
+ynhtest_config_read_yaml() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.yml"
+
+ cat << EOF > $file
+# Some comment
+foo:
+enabled: false
+# title: old title
+title: Lorem Ipsum
+theme: colib'ris
+email: root@example.com # This is a comment without quotes
+port: 1234 # This is a comment without quotes
+url: https://yunohost.org
+dict:
+ ldap_base: ou=users,dc=yunohost,dc=org
+EOF
+
+ test "$(_read_yaml "$file" "foo")" == "None"
+ test "$(ynh_read_var_in_file "$file" "foo")" == ""
+
+ test "$(_read_yaml "$file" "enabled")" == "False"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "false"
+
+ test "$(_read_yaml "$file" "title")" == "Lorem Ipsum"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum"
+
+ test "$(_read_yaml "$file" "theme")" == "colib'ris"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris"
+
+ test "$(_read_yaml "$file" "email")" == "root@example.com"
+ test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com"
+
+ test "$(_read_yaml "$file" "port")" == "1234"
+ test "$(ynh_read_var_in_file "$file" "port")" == "1234"
+
+ test "$(_read_yaml "$file" "url")" == "https://yunohost.org"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org"
+
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org"
+
+ ! _read_yaml "$file" "nonexistent"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! _read_yaml "$file" "enable"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+}
+
+
+ynhtest_config_write_yaml() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.yml"
+
+ cat << EOF > $file
+# Some comment
+foo:
+enabled: false
+# title: old title
+title: Lorem Ipsum
+theme: colib'ris
+email: root@example.com # This is a comment without quotes
+port: 1234 # This is a comment without quotes
+url: https://yunohost.org
+dict:
+ ldap_base: ou=users,dc=yunohost,dc=org
+EOF
+
+ ynh_write_var_in_file "$file" "foo" "bar"
+ # cat $dummy_dir/dummy.yml # to debug
+ ! test "$(_read_yaml "$file" "foo")" == "bar" # writing broke the yaml syntax... "foo:bar" (no space aftr :)
+ test "$(ynh_read_var_in_file "$file" "foo")" == "bar"
+
+ ynh_write_var_in_file "$file" "enabled" "true"
+ test "$(_read_yaml "$file" "enabled")" == "True"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "true"
+
+ ynh_write_var_in_file "$file" "title" "Foo Bar"
+ test "$(_read_yaml "$file" "title")" == "Foo Bar"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar"
+
+ ynh_write_var_in_file "$file" "theme" "super-awesome-theme"
+ test "$(_read_yaml "$file" "theme")" == "super-awesome-theme"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme"
+
+ ynh_write_var_in_file "$file" "email" "sam@domain.tld"
+ test "$(_read_yaml "$file" "email")" == "sam@domain.tld"
+ test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld"
+
+ ynh_write_var_in_file "$file" "port" "5678"
+ test "$(_read_yaml "$file" "port")" == "5678"
+ test "$(ynh_read_var_in_file "$file" "port")" == "5678"
+
+ ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar"
+ test "$(_read_yaml "$file" "url")" == "https://domain.tld/foobar"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar"
+
+ ynh_write_var_in_file "$file" "ldap_base" "ou=foobar,dc=domain,dc=tld"
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=foobar,dc=domain,dc=tld"
+
+ ! ynh_write_var_in_file "$file" "nonexistent" "foobar"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! ynh_write_var_in_file "$file" "enable" "foobar"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "true"
+}
+
+#########################
+# _ #
+# (_) #
+# _ ___ ___ _ __ #
+# | / __|/ _ \| '_ \ #
+# | \__ \ (_) | | | | #
+# | |___/\___/|_| |_| #
+# _/ | #
+# |__/ #
+# #
+#########################
+
+_read_json() {
+ local file="$1"
+ local key="$2"
+ python3 -c "import json; print(json.load(open('$file'))['$key'])"
+}
+
+ynhtest_config_read_json() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.json"
+
+ cat << EOF > $file
+{
+ "foo": null,
+ "enabled": false,
+ "title": "Lorem Ipsum",
+ "theme": "colib'ris",
+ "email": "root@example.com",
+ "port": 1234,
+ "url": "https://yunohost.org",
+ "dict": {
+ "ldap_base": "ou=users,dc=yunohost,dc=org"
+ }
+}
+EOF
+
+
+ test "$(_read_json "$file" "foo")" == "None"
+ test "$(ynh_read_var_in_file "$file" "foo")" == "null"
+
+ test "$(_read_json "$file" "enabled")" == "False"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "false"
+
+ test "$(_read_json "$file" "title")" == "Lorem Ipsum"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum"
+
+ test "$(_read_json "$file" "theme")" == "colib'ris"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris"
+
+ test "$(_read_json "$file" "email")" == "root@example.com"
+ test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com"
+
+ test "$(_read_json "$file" "port")" == "1234"
+ test "$(ynh_read_var_in_file "$file" "port")" == "1234"
+
+ test "$(_read_json "$file" "url")" == "https://yunohost.org"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org"
+
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org"
+
+ ! _read_json "$file" "nonexistent"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! _read_json "$file" "enable"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+}
+
+
+ynhtest_config_write_json() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.json"
+
+ cat << EOF > $file
+{
+ "foo": null,
+ "enabled": false,
+ "title": "Lorem Ipsum",
+ "theme": "colib'ris",
+ "email": "root@example.com",
+ "port": 1234,
+ "url": "https://yunohost.org",
+ "dict": {
+ "ldap_base": "ou=users,dc=yunohost,dc=org"
+ }
+}
+EOF
+
+ ynh_write_var_in_file "$file" "foo" "bar"
+ cat $file
+ test "$(_read_json "$file" "foo")" == "bar"
+ test "$(ynh_read_var_in_file "$file" "foo")" == "bar"
+
+ ynh_write_var_in_file "$file" "enabled" "true"
+ cat $file
+ test "$(_read_json "$file" "enabled")" == "true"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "true"
+
+ ynh_write_var_in_file "$file" "title" "Foo Bar"
+ cat $file
+ test "$(_read_json "$file" "title")" == "Foo Bar"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar"
+
+ ynh_write_var_in_file "$file" "theme" "super-awesome-theme"
+ cat $file
+ test "$(_read_json "$file" "theme")" == "super-awesome-theme"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme"
+
+ ynh_write_var_in_file "$file" "email" "sam@domain.tld"
+ cat $file
+ test "$(_read_json "$file" "email")" == "sam@domain.tld"
+ test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld"
+
+ ynh_write_var_in_file "$file" "port" "5678"
+ test "$(_read_json "$file" "port")" == "5678"
+ test "$(ynh_read_var_in_file "$file" "port")" == "5678"
+
+ ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar"
+ test "$(_read_json "$file" "url")" == "https://domain.tld/foobar"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar"
+
+ ynh_write_var_in_file "$file" "ldap_base" "ou=foobar,dc=domain,dc=tld"
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=foobar,dc=domain,dc=tld"
+
+ ! ynh_write_var_in_file "$file" "nonexistent" "foobar"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! ynh_write_var_in_file "$file" "enable" "foobar"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "true"
+}
+
+#######################
+# _ #
+# | | #
+# _ __ | |__ _ __ #
+# | '_ \| '_ \| '_ \ #
+# | |_) | | | | |_) | #
+# | .__/|_| |_| .__/ #
+# | | | | #
+# |_| |_| #
+# #
+#######################
+
+_read_php() {
+ local file="$1"
+ local key="$2"
+ php -r "include '$file'; echo var_export(\$$key);" | sed "s/^'//g" | sed "s/'$//g"
+}
+
+ynhtest_config_read_php() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.php"
+
+ cat << EOF > $file
+ "ou=users,dc=yunohost,dc=org",
+ 'ldap_conf' => []
+ ];
+ \$dict['ldap_conf']['user'] = 'camille';
+ const DB_HOST = 'localhost';
+?>
+EOF
+
+ test "$(_read_php "$file" "foo")" == "NULL"
+ test "$(ynh_read_var_in_file "$file" "foo")" == "NULL"
+
+ test "$(_read_php "$file" "enabled")" == "false"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "false"
+
+ test "$(_read_php "$file" "title")" == "Lorem Ipsum"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Lorem Ipsum"
+
+ test "$(_read_php "$file" "theme")" == "colib\\'ris"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "colib'ris"
+
+ test "$(_read_php "$file" "email")" == "root@example.com"
+ test "$(ynh_read_var_in_file "$file" "email")" == "root@example.com"
+
+ test "$(_read_php "$file" "port")" == "1234"
+ test "$(ynh_read_var_in_file "$file" "port")" == "1234"
+
+ test "$(_read_php "$file" "url")" == "https://yunohost.org"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://yunohost.org"
+
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=users,dc=yunohost,dc=org"
+
+ test "$(ynh_read_var_in_file "$file" "user")" == "camille"
+
+ test "$(ynh_read_var_in_file "$file" "DB_HOST")" == "localhost"
+
+ ! _read_php "$file" "nonexistent"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! _read_php "$file" "enable"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+}
+
+
+ynhtest_config_write_php() {
+ local dummy_dir="$(mktemp -d -p $VAR_WWW)"
+ file="$dummy_dir/dummy.php"
+
+ cat << EOF > $file
+ "ou=users,dc=yunohost,dc=org",
+ ];
+?>
+EOF
+
+ ynh_write_var_in_file "$file" "foo" "bar"
+ test "$(_read_php "$file" "foo")" == "bar"
+ test "$(ynh_read_var_in_file "$file" "foo")" == "bar"
+
+ ynh_write_var_in_file "$file" "enabled" "true"
+ test "$(_read_php "$file" "enabled")" == "true"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "true"
+
+ ynh_write_var_in_file "$file" "title" "Foo Bar"
+ cat $file
+ test "$(_read_php "$file" "title")" == "Foo Bar"
+ test "$(ynh_read_var_in_file "$file" "title")" == "Foo Bar"
+
+ ynh_write_var_in_file "$file" "theme" "super-awesome-theme"
+ cat $file
+ test "$(_read_php "$file" "theme")" == "super-awesome-theme"
+ test "$(ynh_read_var_in_file "$file" "theme")" == "super-awesome-theme"
+
+ ynh_write_var_in_file "$file" "email" "sam@domain.tld"
+ cat $file
+ test "$(_read_php "$file" "email")" == "sam@domain.tld"
+ test "$(ynh_read_var_in_file "$file" "email")" == "sam@domain.tld"
+
+ ynh_write_var_in_file "$file" "port" "5678"
+ test "$(_read_php "$file" "port")" == "5678"
+ test "$(ynh_read_var_in_file "$file" "port")" == "5678"
+
+ ynh_write_var_in_file "$file" "url" "https://domain.tld/foobar"
+ test "$(_read_php "$file" "url")" == "https://domain.tld/foobar"
+ test "$(ynh_read_var_in_file "$file" "url")" == "https://domain.tld/foobar"
+
+ ynh_write_var_in_file "$file" "ldap_base" "ou=foobar,dc=domain,dc=tld"
+ test "$(ynh_read_var_in_file "$file" "ldap_base")" == "ou=foobar,dc=domain,dc=tld"
+
+ ! ynh_write_var_in_file "$file" "nonexistent" "foobar"
+ test "$(ynh_read_var_in_file "$file" "nonexistent")" == "YNH_NULL"
+
+ ! ynh_write_var_in_file "$file" "enable" "foobar"
+ test "$(ynh_read_var_in_file "$file" "enable")" == "YNH_NULL"
+ test "$(ynh_read_var_in_file "$file" "enabled")" == "true"
+}
diff --git a/tests/test_helpers.d/ynhtest_network.sh b/tests/test_helpers.d/ynhtest_network.sh
new file mode 100644
index 000000000..c1644fc15
--- /dev/null
+++ b/tests/test_helpers.d/ynhtest_network.sh
@@ -0,0 +1,22 @@
+ynhtest_port_80_aint_available() {
+ ! ynh_port_available 80
+}
+
+ynhtest_port_12345_is_available() {
+ ynh_port_available 12345
+}
+
+ynhtest_port_12345_is_booked_by_other_app() {
+
+ ynh_port_available 12345
+ ynh_port_available 12346
+
+ mkdir -p /etc/yunohost/apps/block_port/
+ echo "port: '12345'" > /etc/yunohost/apps/block_port/settings.yml
+ ! ynh_port_available 12345
+
+ echo "other_port: '12346'" > /etc/yunohost/apps/block_port/settings.yml
+ ! ynh_port_available 12346
+
+ rm -rf /etc/yunohost/apps/block_port
+}
diff --git a/tests/test_helpers.d/ynhtest_secure_remove.sh b/tests/test_helpers.d/ynhtest_secure_remove.sh
new file mode 100644
index 000000000..04d85fa7a
--- /dev/null
+++ b/tests/test_helpers.d/ynhtest_secure_remove.sh
@@ -0,0 +1,71 @@
+ynhtest_acceptable_path_to_delete() {
+
+ mkdir -p /home/someuser
+ mkdir -p /home/$app
+ mkdir -p /home/yunohost.app/$app
+ mkdir -p /var/www/$app
+ touch /var/www/$app/bar
+ touch /etc/cron.d/$app
+
+ ! _acceptable_path_to_delete /
+ ! _acceptable_path_to_delete ////
+ ! _acceptable_path_to_delete " //// "
+ ! _acceptable_path_to_delete /var
+ ! _acceptable_path_to_delete /var/www
+ ! _acceptable_path_to_delete /var/cache
+ ! _acceptable_path_to_delete /usr
+ ! _acceptable_path_to_delete /usr/bin
+ ! _acceptable_path_to_delete /home
+ ! _acceptable_path_to_delete /home/yunohost.backup
+ ! _acceptable_path_to_delete /home/yunohost.app
+ ! _acceptable_path_to_delete /home/yunohost.app/
+ ! _acceptable_path_to_delete ///home///yunohost.app///
+ ! _acceptable_path_to_delete /home/yunohost.app/$app/..
+ ! _acceptable_path_to_delete ///home///yunohost.app///$app///..//
+ ! _acceptable_path_to_delete /home/yunohost.app/../$app/..
+ ! _acceptable_path_to_delete /home/someuser
+ ! _acceptable_path_to_delete /home/yunohost.app//../../$app
+ ! _acceptable_path_to_delete " /home/yunohost.app/// "
+ ! _acceptable_path_to_delete /etc/cron.d/
+ ! _acceptable_path_to_delete /etc/yunohost/
+
+ _acceptable_path_to_delete /home/yunohost.app/$app
+ _acceptable_path_to_delete /home/yunohost.app/$app/bar
+ _acceptable_path_to_delete /etc/cron.d/$app
+ _acceptable_path_to_delete /var/www/$app/bar
+ _acceptable_path_to_delete /var/www/$app
+
+ rm /var/www/$app/bar
+ rm /etc/cron.d/$app
+ rmdir /home/yunohost.app/$app
+ rmdir /home/$app
+ rmdir /home/someuser
+ rmdir /var/www/$app
+}
+
+ynhtest_secure_remove() {
+
+ mkdir -p /home/someuser
+ mkdir -p /home/yunohost.app/$app
+ mkdir -p /var/www/$app
+ mkdir -p /var/whatever
+ touch /var/www/$app/bar
+ touch /etc/cron.d/$app
+
+ ! ynh_secure_remove --file="/home/someuser"
+ ! ynh_secure_remove --file="/home/yunohost.app/"
+ ! ynh_secure_remove --file="/var/whatever"
+ ynh_secure_remove --file="/home/yunohost.app/$app"
+ ynh_secure_remove --file="/var/www/$app"
+ ynh_secure_remove --file="/etc/cron.d/$app"
+
+ test -e /home/someuser
+ test -e /home/yunohost.app
+ test -e /var/whatever
+ ! test -e /home/yunohost.app/$app
+ ! test -e /var/www/$app
+ ! test -e /etc/cron.d/$app
+
+ rmdir /home/someuser
+ rmdir /var/whatever
+}
diff --git a/tests/test_helpers.d/ynhtest_setup_source.sh b/tests/test_helpers.d/ynhtest_setup_source.sh
new file mode 100644
index 000000000..fe61e7401
--- /dev/null
+++ b/tests/test_helpers.d/ynhtest_setup_source.sh
@@ -0,0 +1,80 @@
+_make_dummy_src() {
+ if [ ! -e $HTTPSERVER_DIR/dummy.tar.gz ]
+ then
+ pushd "$HTTPSERVER_DIR"
+ mkdir dummy
+ pushd dummy
+ echo "Lorem Ipsum" > index.html
+ echo '{"foo": "bar"}' > conf.json
+ mkdir assets
+ echo '.some.css { }' > assets/main.css
+ echo 'var some="js";' > assets/main.js
+ popd
+ tar -czf dummy.tar.gz dummy
+ popd
+ fi
+ echo "SOURCE_URL=http://127.0.0.1:$HTTPSERVER_PORT/dummy.tar.gz"
+ echo "SOURCE_SUM=$(sha256sum $HTTPSERVER_DIR/dummy.tar.gz | awk '{print $1}')"
+}
+
+ynhtest_setup_source_nominal() {
+ final_path="$(mktemp -d -p $VAR_WWW)"
+ _make_dummy_src > ../conf/dummy.src
+
+ ynh_setup_source --dest_dir="$final_path" --source_id="dummy"
+
+ test -e "$final_path"
+ test -e "$final_path/index.html"
+}
+
+ynhtest_setup_source_nominal_upgrade() {
+ final_path="$(mktemp -d -p $VAR_WWW)"
+ _make_dummy_src > ../conf/dummy.src
+
+ ynh_setup_source --dest_dir="$final_path" --source_id="dummy"
+
+ test "$(cat $final_path/index.html)" == "Lorem Ipsum"
+
+ # Except index.html to get overwritten during next ynh_setup_source
+ echo "IEditedYou!" > $final_path/index.html
+ test "$(cat $final_path/index.html)" == "IEditedYou!"
+
+ ynh_setup_source --dest_dir="$final_path" --source_id="dummy"
+
+ test "$(cat $final_path/index.html)" == "Lorem Ipsum"
+}
+
+
+ynhtest_setup_source_with_keep() {
+ final_path="$(mktemp -d -p $VAR_WWW)"
+ _make_dummy_src > ../conf/dummy.src
+
+ echo "IEditedYou!" > $final_path/index.html
+ echo "IEditedYou!" > $final_path/test.txt
+
+ ynh_setup_source --dest_dir="$final_path" --source_id="dummy" --keep="index.html test.txt"
+
+ test -e "$final_path"
+ test -e "$final_path/index.html"
+ test -e "$final_path/test.txt"
+ test "$(cat $final_path/index.html)" == "IEditedYou!"
+ test "$(cat $final_path/test.txt)" == "IEditedYou!"
+}
+
+ynhtest_setup_source_with_patch() {
+ final_path="$(mktemp -d -p $VAR_WWW)"
+ _make_dummy_src > ../conf/dummy.src
+
+ mkdir -p ../sources/patches
+ cat > ../sources/patches/dummy-index.html.patch << EOF
+--- a/index.html
++++ b/index.html
+@@ -1 +1,1 @@
+-Lorem Ipsum
++Lorem Ipsum dolor sit amet
+EOF
+
+ ynh_setup_source --dest_dir="$final_path" --source_id="dummy"
+
+ test "$(cat $final_path/index.html)" == "Lorem Ipsum dolor sit amet"
+}
diff --git a/tests/test_helpers.sh b/tests/test_helpers.sh
new file mode 100644
index 000000000..153ce1386
--- /dev/null
+++ b/tests/test_helpers.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+
+readonly NORMAL=$(printf '\033[0m')
+readonly BOLD=$(printf '\033[1m')
+readonly RED=$(printf '\033[31m')
+readonly GREEN=$(printf '\033[32m')
+readonly ORANGE=$(printf '\033[33m')
+
+function log_test()
+{
+ echo -n "${BOLD}$1${NORMAL} ... "
+}
+
+function log_passed()
+{
+ echo "${BOLD}${GREEN}✔ Passed${NORMAL}"
+}
+
+function log_failed()
+{
+ echo "${BOLD}${RED}✘ Failed${NORMAL}"
+}
+
+function cleanup()
+{
+ [ -n "$HTTPSERVER" ] && kill "$HTTPSERVER"
+ [ -d "$HTTPSERVER_DIR" ] && rm -rf "$HTTPSERVER_DIR"
+ [ -d "$VAR_WWW" ] && rm -rf "$VAR_WWW"
+}
+trap cleanup EXIT SIGINT
+
+# =========================================================
+
+# Dummy http server, to serve archives for ynh_setup_source
+HTTPSERVER_DIR=$(mktemp -d)
+HTTPSERVER_PORT=1312
+pushd "$HTTPSERVER_DIR" >/dev/null
+python3 -m http.server $HTTPSERVER_PORT --bind 127.0.0.1 &>/dev/null &
+HTTPSERVER="$!"
+popd >/dev/null
+
+VAR_WWW=$(mktemp -d)/var/www
+mkdir -p $VAR_WWW
+# =========================================================
+
+for TEST_SUITE in $(ls test_helpers.d/*)
+do
+ source $TEST_SUITE
+done
+
+# Hack to list all known function, keep only those starting by ynhtest_
+TESTS=$(declare -F | grep ' ynhtest_' | awk '{print $3}')
+
+global_result=0
+
+for TEST in $TESTS
+do
+ log_test $TEST
+ cd $(mktemp -d)
+ (mkdir conf
+ mkdir scripts
+ cd scripts
+ source /usr/share/yunohost/helpers
+ app=ynhtest
+ YNH_APP_ID=$app
+ set -eux
+ $TEST
+ ) > ./test.log 2>&1
+
+ if [[ $? == 0 ]]
+ then
+ set +x; log_passed;
+ else
+ set +x; echo -e "\n----------"; cat ./test.log; echo -e "----------"; log_failed; global_result=1;
+ fi
+done
+
+exit $global_result
diff --git a/tests/test_i18n_keys.py b/tests/test_i18n_keys.py
index 6876cbcd8..103241085 100644
--- a/tests/test_i18n_keys.py
+++ b/tests/test_i18n_keys.py
@@ -6,14 +6,7 @@ import glob
import json
import yaml
import subprocess
-
-ignore = [
- "password_too_simple_",
- "password_listed",
- "backup_method_",
- "backup_applying_method_",
- "confirm_app_install_",
-]
+import toml
###############################################################################
# Find used keys in python code #
@@ -25,14 +18,17 @@ def find_expected_string_keys():
# Try to find :
# m18n.n( "foo"
# YunohostError("foo"
+ # YunohostValidationError("foo"
# # i18n: foo
p1 = re.compile(r"m18n\.n\(\n*\s*[\"\'](\w+)[\"\']")
p2 = re.compile(r"YunohostError\(\n*\s*[\'\"](\w+)[\'\"]")
- p3 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?")
+ p3 = re.compile(r"YunohostValidationError\(\n*\s*[\'\"](\w+)[\'\"]")
+ p4 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?")
python_files = glob.glob("src/yunohost/*.py")
python_files.extend(glob.glob("src/yunohost/utils/*.py"))
python_files.extend(glob.glob("src/yunohost/data_migrations/*.py"))
+ python_files.extend(glob.glob("src/yunohost/authenticators/*.py"))
python_files.extend(glob.glob("data/hooks/diagnosis/*.py"))
python_files.append("bin/yunohost")
@@ -47,6 +43,10 @@ def find_expected_string_keys():
continue
yield m
for m in p3.findall(content):
+ if m.endswith("_"):
+ continue
+ yield m
+ for m in p4.findall(content):
yield m
# For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries)
@@ -102,7 +102,7 @@ def find_expected_string_keys():
yield m
# Keys for the actionmap ...
- for category in yaml.load(open("data/actionsmap/yunohost.yml")).values():
+ for category in yaml.safe_load(open("data/actionsmap/yunohost.yml")).values():
if "actions" not in category.keys():
continue
for action in category["actions"].values():
@@ -130,33 +130,23 @@ def find_expected_string_keys():
yield "backup_applying_method_%s" % method
yield "backup_method_%s_finished" % method
- for level in ["danger", "thirdparty", "warning"]:
- yield "confirm_app_install_%s" % level
+ registrars = toml.load(open("data/other/registrar_list.toml"))
+ supported_registrars = ["ovh", "gandi", "godaddy"]
+ for registrar in supported_registrars:
+ for key in registrars[registrar].keys():
+ yield f"domain_config_{key}"
- for errortype in ["not_found", "error", "warning", "success", "not_found_details"]:
- yield "diagnosis_domain_expiration_%s" % errortype
- yield "diagnosis_domain_not_found_details"
-
- for errortype in ["bad_status_code", "connection_error", "timeout"]:
- yield "diagnosis_http_%s" % errortype
-
- yield "password_listed"
- for i in [1, 2, 3, 4]:
- yield "password_too_simple_%s" % i
-
- checks = [
- "outgoing_port_25_ok",
- "ehlo_ok",
- "fcrdns_ok",
- "blacklist_ok",
- "queue_ok",
- "ehlo_bad_answer",
- "ehlo_unreachable",
- "ehlo_bad_answer_details",
- "ehlo_unreachable_details",
- ]
- for check in checks:
- yield "diagnosis_mail_%s" % check
+ domain_config = toml.load(open("data/other/config_domain.toml"))
+ for panel in domain_config.values():
+ if not isinstance(panel, dict):
+ continue
+ for section in panel.values():
+ if not isinstance(section, dict):
+ continue
+ for key, values in section.items():
+ if not isinstance(values, dict):
+ continue
+ yield f"domain_config_{key}"
###############################################################################
diff --git a/tox.ini b/tox.ini
index c25d8bf8f..267134e57 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,8 +6,10 @@ skip_install=True
deps =
py37-{lint,invalidcode}: flake8
py37-black-{run,check}: black
+ py37-mypy: mypy >= 0.900
commands =
py37-lint: flake8 src doc data tests --ignore E402,E501,E203,W503 --exclude src/yunohost/vendor
- py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F
+ py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F,E722,W605
py37-black-check: black --check --diff src doc data tests
py37-black-run: black src doc data tests
+ py37-mypy: mypy --ignore-missing-import --install-types --non-interactive --follow-imports silent src/yunohost/ --exclude (acme_tiny|data_migrations)