From 6e4c9b1db2155f479919e12c84f91fa89334d8a7 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 20 Mar 2018 12:55:26 +0200 Subject: translations PR 20180319 (#8479) --- i18n/de.json | 84 +++++++++++++++++++++++++++++++ i18n/en.json | 152 ++++++++++++++++++++++++++++---------------------------- i18n/es.json | 84 +++++++++++++++++++++++++++++++ i18n/fr.json | 84 +++++++++++++++++++++++++++++++ i18n/it.json | 84 +++++++++++++++++++++++++++++++ i18n/ja.json | 86 +++++++++++++++++++++++++++++++- i18n/ko.json | 86 +++++++++++++++++++++++++++++++- i18n/nl.json | 84 +++++++++++++++++++++++++++++++ i18n/pl.json | 84 +++++++++++++++++++++++++++++++ i18n/pt-BR.json | 84 +++++++++++++++++++++++++++++++ i18n/ru.json | 84 +++++++++++++++++++++++++++++++ i18n/tr.json | 102 +++++++++++++++++++++++++++++++++---- i18n/zh-CN.json | 84 +++++++++++++++++++++++++++++++ i18n/zh-TW.json | 84 +++++++++++++++++++++++++++++++ 14 files changed, 1179 insertions(+), 87 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index 2a7952315..f3703fbd5 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - E-Mail-Einstellungen überprüfen" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3-Bucket wird benötigt" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3-Endpunkt wird benötigt" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3-Region wird benötigt" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Leeres Array unterhalb von 'image' in der Anfrage" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v wurde aus dem Team entfernt." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Leeres Array unterhalb von 'image' in der Anfrage" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Keine Datei unter 'image' in der Anfrage" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Konnte Bild nicht öffnen" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Kann Multipart-Formular nicht analysieren" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Die Datei kann nicht hochgeladen werden. Ein Foto Speicherort ist nicht eingerichtet." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Bildupload nicht möglich. Datei ist zu groß." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "Team-Anmeldung mit einer E-Mail-Adresse ist deaktiviert." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Kann extrahiertes Plugin nicht aktivieren. Plugin könnte schon existieren und aktiviert sein." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Fehler beim Speichern des Plugin-Status in der Konfiguration" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "'ExportFormat' des Nachrichten-Export-Jobs muss 'actiance' oder 'globalrelay' sein." }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "'ExportFormat' des Nachrichten-Export-Jobs muss 'actiance' oder 'globalrelay' sein." + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Nachrichten-Export-Aufgabe \"FileLocation\" muss ein beschreibbares Verzeichnis sein in welches die Exportdaten geschrieben werden" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Nachrichten-Export-Aufgabe \"FileLocation\" muss ein Unterverzeichnis von \"FileSettings.Directory\" sein" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Nachrichten-Export-Job GlobalRelayEmailAddress muss eine gültige E-Mail-Adresse sein." + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Nachrichten-Export-Job GlobalRelayEmailAddress muss eine gültige E-Mail-Adresse sein." diff --git a/i18n/en.json b/i18n/en.json index 900b55170..42650a868 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -111,14 +111,14 @@ "id": "api.admin.test_s3.missing_s3_bucket", "translation": "S3 Bucket is required" }, - { - "id": "api.admin.test_s3.missing_s3_region", - "translation": "S3 Region is required" - }, { "id": "api.admin.test_s3.missing_s3_endpoint", "translation": "S3 Endpoint is required" }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Empty array under 'image' in request" @@ -2198,50 +2198,6 @@ "id": "api.system.go_routines", "translation": "The number of running goroutines is over the health threshold %v of %v" }, - { - "id": "api.team.set_team_icon.get_team.app_error", - "translation": "An error occurred getting the team" - }, - { - "id": "api.team.set_team_icon.storage.app_error", - "translation": "Unable to upload team icon. Image storage is not configured." - }, - { - "id": "api.team.set_team_icon.too_large.app_error", - "translation": "Unable to upload team icon. File is too large." - }, - { - "id": "api.team.set_team_icon.parse.app_error", - "translation": "Could not parse multipart form" - }, - { - "id": "api.team.set_team_icon.no_file.app_error", - "translation": "No file under 'image' in request" - }, - { - "id": "api.team.set_team_icon.array.app_error", - "translation": "Empty array under 'image' in request" - }, - { - "id": "api.team.set_team_icon.open.app_error", - "translation": "Could not open image file" - }, - { - "id": "api.team.set_team_icon.decode_config.app_error", - "translation": "Could not decode team icon metadata" - }, - { - "id": "api.team.set_team_icon.decode.app_error", - "translation": "Could not decode team icon" - }, - { - "id": "api.team.set_team_icon.encode.app_error", - "translation": "Could not encode team icon" - }, - { - "id": "api.team.set_team_icon.write_file.app_error", - "translation": "Could not save team icon" - }, { "id": "api.team.add_user_to_team.added", "translation": "%v added to the team by %v." @@ -2386,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v removed from the team." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Empty array under 'image' in request" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "No file under 'image' in request" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Could not open image file" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Could not parse multipart form" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Unable to upload team icon. Image storage is not configured." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Unable to upload team icon. File is too large." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "Team sign-up with email is disabled." @@ -3702,14 +3702,14 @@ "id": "app.plugin.activate.app_error", "translation": "Unable to activate extracted plugin. Plugin may already exist and be activated." }, - { - "id": "app.plugin.config.app_error", - "translation": "Error saving plugin state in config" - }, { "id": "app.plugin.cluster.save_config.app_error", "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." }, + { + "id": "app.plugin.config.app_error", + "translation": "Error saving plugin state in config" + }, { "id": "app.plugin.deactivate.app_error", "translation": "Unable to deactivate plugin" @@ -4950,30 +4950,6 @@ "id": "model.config.is_valid.message_export.batch_size.app_error", "translation": "Message export job BatchSize must be a positive integer" }, - { - "id": "model.config.is_valid.message_export.export_type.app_error", - "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" - }, - { - "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", - "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" - }, - { - "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", - "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" - }, - { - "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", - "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" - }, - { - "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", - "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" - }, - { - "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", - "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" - }, { "id": "model.config.is_valid.message_export.daily_runtime.app_error", "translation": "Message export job DailyRuntime must be a 24-hour time stamp in the form HH:MM." @@ -4990,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Message export job FileLocation must be a writable directory that export data will be written to" @@ -4998,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" diff --git a/i18n/es.json b/i18n/es.json index e6a721f68..49d0550b6 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - Configuración de pruebas de correo" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket es necesario" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint es necesario" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Región es necesaria" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Arreglo vacio bajo 'image' en la solicitud" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v removido del equipo." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Arreglo vacío bajo 'image' en la solicitud" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "No se pudo descodificar el icono de equipo" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "No se pudo descodificar la metadata del icono de equipo" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "No se pudo codificar el icono de equipo" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Ocurrió un error al obtener el equipo" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "No hay un archivo bajo 'image' en la solicitud" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "No se pudo abrir el archivo de imagen" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "No se pudo analizar el formulario multipart" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "No se puede cargar el icono del equipo. El almacenamiento de imágenes no está configurado." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "No se pudo subir el icono del equipo. El archivo es demasiado grande." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "No se pudo guardar el icono del equipo" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "El registro a equipos por correo electrónico está inhabilitado." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "No se puede activar el plugin extraído. Puede que el plugin ya exista y esté activo." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "La configuración de plugin en tu archivo config.json debe ser actualizado manualmente cuando se utiliza ReadOnlyConfig con el agrupamiento de servidores habilitado." + }, { "id": "app.plugin.config.app_error", "translation": "Error al guardar el estado del plugin en la configuración" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "El trabajo de exportación de Mensaje ExportFormat debe ser 'actiance' o 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "El trabajo de exportación de Mensaje ExportFormat debe ser 'actiance' o 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "El ajuste FileLocation para realizar el trabajo de Exportación de Mensajes debe ser un directorio con permiso de escritura donde la data de exportación será almacenada." @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "El ajuste FileLocation para realizar el trabajo de Exportación de Mensajes debe ser un sub-directorio de FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "El trabajo de exportación de mensajes ExportFormat está asignado a 'globalrelay', pero falta establecer los parámetros de GlobalRelaySettings" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "El parámetro de exportación de mensajes GlobalRelaySettings.CustomerType debe ser establecido en 'A9' o 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "El parámetro de exportación de mensajes GlobalRelaySettings.EmailAddress debe estar asignado a una dirección de correo electrónico válida" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "El parámetro de exportación de mensajes GlobalRelaySettings.SmtpPassword debe estar asignado" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "El parámetro de exportación de mensajes GlobalRelaySettings.SmtpUsername debe estar asignado" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "El trabajo de exportación de Mensaje GlobalRelayEmailAddress debe estar asignado a una dirección de correo electrónico válido." diff --git a/i18n/fr.json b/i18n/fr.json index 3a9fae840..dea342bd4 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - Test des paramètres e-mail" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Aucune image transmise dans la requête" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v a été retiré de l'équipe." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Aucune image transmise dans la requête" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Pas de fichier dans le paramètre \"image\" de la requête" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Impossible d'ouvrir le fichier image" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Impossible d'analyser le formulaire multipart" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Impossible d'envoyer le fichier. Le stockage d'images n'est pas configuré." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Impossible d'envoyer le fichier. Le fichier est trop volumineux." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "L'inscription avec une adresse e-mail est désactivée." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Impossible d'activer le plugin extrait. Il se peut qu'il existe déjà et soit déjà activé." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Une erreur s'est produite lors de la sauvegarde de l'état du plugin dans la configuration" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Le paramètre FileLocation de la tâche d'exportation de messages doit être un dossier avec droits d'écriture. Il s'agit du dossier dans lequel les données seront exportées." @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Le paramètre FileLocation de la tâche d'exportation de messages doit être un sous-dossier de FileSettings.Directory." }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" diff --git a/i18n/it.json b/i18n/it.json index 5cb1c8a85..3a6f13f98 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - Test delle impostazioni Email" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "Bucket S3 richiesto" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "Endpoint S3 richiesto" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "Region S3 richiesta" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Lista vuota per il campo 'image' nella richiesta" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v è stato rimosso dal gruppo." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "La richiesta contiene una lista vuota nel campo 'image'" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Impossibile decodificare l'icona del gruppo" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Impossibile decodificare i metadati dell'icona del gruppo" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Impossibile codificare l'icona del gruppo" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Si è verificato un errore recuperando il gruppo" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "La richiesta non contiene file nel campo 'image'" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Impossibile aprire il file immagine" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Errore durante l'analisi della form multipart" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Impossibile caricare l'icona del gruppo. Lo spazio di archiviazione delle immagini non è configurato." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Impossibile caricare l'icona del gruppo. Il file è troppo grande." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Impossibile salvare l'icona del gruppo" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "L'iscrizione al gruppo con email è disabilitata." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Impossibile attivare il plugin estratto. Il plugin può essere già disponibile e dev'essere attivato." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Errore durante il salvataggio della configurazione del plugin" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "ExportFormat del lavoro di esportazione messaggi deve essere 'actianve' oppure 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "ExportFormat del lavoro di esportazione messaggi deve essere 'actianve' oppure 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Lavoro esportazione messaggi il valore FileLocation deve essere una cartella accessibile in scrittura in cui verranno salvati i dati esportati" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Lavoro esportazione messaggi il valore FileLocation deve essere una sottocartella di FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "GlobalRelayEmailAddress del lavoro di esportazione messaggi deve essere un indirizzo email valido" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "GlobalRelayEmailAddress del lavoro di esportazione messaggi deve essere un indirizzo email valido" diff --git a/i18n/ja.json b/i18n/ja.json index b3e90c61f..ecf4ccbe2 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - 電子メール設定のテスト" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3バケット名が必要です" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3エンドポイントが必要です" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3リージョンが必要です" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "リクエストの'image'以下の配列が空です" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v はチームから削除されました。" }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "リクエストの'image'以下の配列が空です" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "チームアイコンをデコードできませんでした" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "チームアイコンメタデータをデコードできませんでした" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "チームアイコンをエンコードできませんでした" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "チームの取得中にエラーが発生しました" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "リクエストの'image'以下にファイルがありません" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "画像ファイルを開けません" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "マルチパートフォームを解析できませんでした" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "チームアイコンをアップロードできません。画像ストレージが設定されていません。" + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "チームアイコンをアップロードできません。ファイルが大き過ぎます。" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "チームアイコンを保存できませんでした" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "電子メールによるチームへの利用登録は無効です。" @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "抽出されたプラグインを有効化できませんでした。プラグインが既に存在し、有効化されている可能性があります。" }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "クラスター化を有効にするとともにReadOnlyConfigを使用する際は、config.jsonファイルのプラグイン設定は手動で更新する必要があります。" + }, { "id": "app.plugin.config.app_error", "translation": "設定にプラグインの状態を保存する際にエラーが発生しました" @@ -4144,7 +4204,7 @@ }, { "id": "ent.metrics.stopping.info", - "translation": "メトリクスとプロファイリングのサーバーは%vで停止しています" + "translation": "%v のメトリクスとプロファイリング用のサーバーは停止しています" }, { "id": "ent.mfa.activate.authenticate.app_error", @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "メッセージエクスポート処理の ExportFormat は 'actiance' か 'globalrelay' のいずれかでなくてはなりません" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "メッセージエクスポート処理の ExportFormat は 'actiance' か 'globalrelay' のいずれかでなくてはなりません" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "メッセーエクスポート処理の FileLocation はエクスポートデータが書き込まれる書き込み可能なディレクトリでなくてはなりません" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "メッセージエクスポート処理の FileLocation は FileSettings.Directory のサブディレクトリでなくてはなりません" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "メッセージエクスポート処理のExportFormatは 'globalrelay' に設定されていますが、GlobalRealySettingsが見つかりません。" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "メッセージエクスポートのGlobalRelaySettings.CustomerTypeには 'A9' か 'A10' のいずれかを設定してください。" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "メッセージエクスポート処理の GlobalRelay.EmailAddress には有効な電子メールアドレスが設定されていなくてはなりません" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "メッセージエクスポート処理のGlobalRelaySettings.SmtpPasswordが設定されていなければなりません。" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "メッセージエクスポート処理のGlobalRelaySettings.SmtpUsernameが設定されていなくてはなりません。" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "メッセージエクスポート処理の GlobalRelayEmailAddress には有効な電子メールアドレスが設定されていなくてはなりません" diff --git a/i18n/ko.json b/i18n/ko.json index 13ba19b79..018ce5c00 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - 이메일 설정 테스트중" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "요청의 'image' 속성에 빈 배열이 전달되었습니다" @@ -641,7 +653,7 @@ }, { "id": "api.command_dnd.desc", - "translation": "Do not disturb disables desktop and mobile push notifications." + "translation": "방해 금지모드는 데스크탑과 모바일 푸시 알림을 비활성화합니다." }, { "id": "api.command_dnd.disabled", @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v 가 채널에서 제거되었습니다." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "요청의 'image' 속성에 빈 배열이 전달되었습니다" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "요청의 'image' 속성 아래에 파일이 없습니다" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "이미지 파일을 열 수 없습니다." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "잘못된 multipart form을 파싱할 수 없습니다." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "파일을 업로드할 수 없습니다. 이미지 저장소가 설정되지 않았습니다." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "이미지 파일을 불러올 수 없습니다. 파일이 너무 큽니다." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "이메일을 통한 팀 가입이 비활성화 되어있습니다." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Unable to activate extracted plugin. Plugin may already exist and be activated." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Error saving plugin state in config" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Message export job FileLocation must be a writable directory that export data will be written to" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" diff --git a/i18n/nl.json b/i18n/nl.json index 1777b59d1..7d24fdb45 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - E-mail-instellingen testen" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Lege array bij 'image' in aanvraag" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v was verwijdert van het kanaal." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Lege array bij 'image' in aanvraag" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Geen bestand bij 'image' in aanvraag" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Kan licentie bestand niet openen" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Kan het 'multipart'-formulier niet verwerken" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Kan bestand niet uploaden. Afbeeldings opslag is niet geconfigureerd." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Kan bestand niet uploaden. Bestand is te groot." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "Team aanmelding via e-mail is uitgeschakeld." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Unable to activate extracted plugin. Plugin may already exist and be activated." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Error saving plugin state in config" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Message export job FileLocation must be a writable directory that export data will be written to" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" diff --git a/i18n/pl.json b/i18n/pl.json index cf1911446..ee3d90055 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - Testowanie ustawień e-mail" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Pusta tablica w kluczu 'image' w żądaniu" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v został usunięty z zespołu." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Pusta tablica w kluczu 'image' w żądaniu" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Brak pliku w polu 'image' w żądaniu" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Nie można otworzyć pliku obrazu" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Nie udało się przetworzyć wieloczęściowego formularza" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Nie można wgrać pliku. Przechowywanie obrazów nie jest skonfigurowane." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Nie można wgrać pliku obrazu. Plik jest zbyt duży." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "Zapisy do zespołu za pomocą email są wyłączone." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Unable to activate extracted plugin. Plugin may already exist and be activated." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Error saving plugin state in config" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Message export job FileLocation must be a writable directory that export data will be written to" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 000d87a0c..87f5590c3 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - Teste de Configurações de Email" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket é obrigatório" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint é obrigatório" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region é obrigatório" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Matriz vazia em 'image' na requisição" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v foi removido da equipe." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Matriz vazia em 'image' na requisição" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Não foi possível ler o ícone da equipe" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Não foi possível ler os metadados do ícone da equipe" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Não foi possível codificar o ícone da equipe" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Ocorreu um erro ao obter a equipe" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Nenhum arquivo em 'image' na requisição" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Não foi possível abrir arquivo de imagem" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Não foi possível processar o formulário multipart" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Não é possível enviar o ícone para equipe. Armazenamento de imagem não está configurado." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Não é possível enviar o ícone para equipe. Arquivo muito grande." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "Inscrição com e-mail na equipe está desativado." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Não foi possível ativar o plugin extraído. O plugin já pode existir e estar ativado." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Falha ao salvar o estado do plugin na configuração" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "Na tarefa de exportação de mensagens o ExportFormat deve ser 'actiance' ou 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Na tarefa de exportação de mensagens o ExportFormat deve ser 'actiance' ou 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Trabalho de exportação de mensagens FileLocation deve ser um diretório gravável onde os dados de exportação serão gravados" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Trabalho de exportação de mensagens FileLocation deve ser um subdiretório de FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Na tarefa de exportação de mensagens o GlobalRelaySettings.EmailAddress deve ser um endereço de email válido" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Na tarefa de exportação de mensagens o GlobalRelayEmailAddress deve ser um endereço de email válido" diff --git a/i18n/ru.json b/i18n/ru.json index 2d2f9d91f..550176c0a 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - Тестирование настроек электронной почты" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "Пустой массив 'image' в запросе" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": " %v удален из команды." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Пустой массив 'image' в запросе" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Нет файла 'image' в запросе" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Не могу открыть файл изображения" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Невозможно обработать форму из нескольких частей" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Не удалось загрузить файл. Хранилище изображений не настроено." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Невозможно загрузить изображение. Файл слишком большой." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "Вход в команду с электронной почтой отключен." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Unable to activate extracted plugin. Plugin may already exist and be activated." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "Error saving plugin state in config" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "Message export job FileLocation must be a writable directory that export data will be written to" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "Message export job FileLocation must be a sub-directory of FileSettings.Directory" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Message export job GlobalRelaySettings.EmailAddress must be set to a valid email address" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address" diff --git a/i18n/tr.json b/i18n/tr.json index 8eb6e20e2..576221cbc 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - E-posta Ayarları Sınanıyor" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Buketi zorunludur" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Bitiş Noktası zorunludur" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Bölgesi zorunludur" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "İstekte 'görsel' altında boş dizi" @@ -629,7 +641,7 @@ }, { "id": "api.command_collapse.desc", - "translation": "Görsel önizlemeleri otomatik daraltılsın" + "translation": "Görsel ön izlemeleri otomatik daraltılsın" }, { "id": "api.command_collapse.name", @@ -689,7 +701,7 @@ }, { "id": "api.command_expand.desc", - "translation": "Görsel önizlemesi otomatik daraltılmasın" + "translation": "Görsel ön izlemeleri otomatik daraltılmasın" }, { "id": "api.command_expand.name", @@ -701,7 +713,7 @@ }, { "id": "api.command_expand_collapse.fail.app_error", - "translation": "Önizlemeler genişletilirken bir sorun çıktı" + "translation": "Ön izlemeler genişletilirken bir sorun çıktı" }, { "id": "api.command_groupmsg.desc", @@ -1222,7 +1234,7 @@ }, { "id": "api.file.get_file_preview.no_preview.app_error", - "translation": "Dosyanın önizleme görseli yok" + "translation": "Dosyanın ön izleme görseli yok" }, { "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", @@ -1262,11 +1274,11 @@ }, { "id": "api.file.handle_images_forget.encode_preview.error", - "translation": "Görsel JPG önizlemesi olarak kodlanamadı. Yol: %v Hata: %v" + "translation": "Görsel JPG ön izlemesi olarak kodlanamadı. Yol: %v Hata: %v" }, { "id": "api.file.handle_images_forget.upload_preview.error", - "translation": "Önizleme yüklenemedi. Yol: %v Hata: %v" + "translation": "Ön izleme yüklenemedi. Yol: %v Hata: %v" }, { "id": "api.file.handle_images_forget.upload_thumb.error", @@ -1768,7 +1780,7 @@ }, { "id": "api.post.link_preview_disabled.app_error", - "translation": "Bağlantı önizleme özelliği sistem yöneticisi tarafından devre dışı bırakılmış." + "translation": "Bağlantı ön izleme özelliği sistem yöneticisi tarafından devre dışı bırakılmış." }, { "id": "api.post.make_direct_channel_visible.get_2_members.error", @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v takımdan çıkarıldı." }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "İstekte 'görsel' altındaki dizi boş" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Takım simgesinin kodu çözülemedi" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Takım simgesi üst verisinin kodu çözülemedi" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Takım simgesi kodlanamadı" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Takım alınırken bir sorun çıktı" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "İstekte 'görsel' altında dosya yok" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Görsel dosyası açılamadı" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Çok parçalı form işlenemedi" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Takım simgesi yüklenemedi. Görsel deposu ayarları yapılmamış." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Takım simgesi yüklenemedi. Dosya çok büyük." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Takım simgesi kaydedilemedi" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "E-posta ile takım hesabı açma özelliği devre dışı bırakılmış." @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "Ayıklanan uygulama eki etkinleştirilemedi. Uygulama eki zaten var ve etkinleştirilmiş olabilir." }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "Küme özelliği kullanılırken ve SaltOkunurYapılandırma seçeneği etkinken config.json dosyasındaki uygulama eki yapılandırması el ile güncellenmelidir" + }, { "id": "app.plugin.config.app_error", "translation": "Uygulama eki durumu yapılandırmaya kaydedilirken sorun çıktı" @@ -4764,11 +4824,11 @@ }, { "id": "model.config.is_valid.file_preview_height.app_error", - "translation": "Önizleme yüksekliği dosya ayarı geçersiz. Sıfır ya da pozitif bir sayı olmalı." + "translation": "Ön izleme yüksekliği dosya ayarı geçersiz. Sıfır ya da pozitif bir sayı olmalı." }, { "id": "model.config.is_valid.file_preview_width.app_error", - "translation": "Önizleme genişliği dosya ayarı geçersiz. Sıfır ya da pozitif bir sayı olmalı." + "translation": "Ön izleme genişliği dosya ayarı geçersiz. Sıfır ya da pozitif bir sayı olmalı." }, { "id": "model.config.is_valid.file_profile_height.app_error", @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "İleti dışa aktarma görevinin Dışa Aktarma Biçimi 'actiance' ya da 'genelaktarım' olmalıdır" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "İleti dışa aktarma görevinin Dışa Aktarma Biçimi 'actiance' ya da 'genelaktarım' olmalıdır" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "İleti dışa aktarma görevi DosyaKonumu değeri dışa aktarılacak verilerin kaydedilebilmesi için yazılabilen bir klasör olmalıdır" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "İleti dışa aktarma görevi DosyaKonumu DosyaAyarları.Klasör değerinin bir alt klasörü olmalıdır" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "İleti dışa aktarma görevi DışaAktarmaBiçimi 'GenelAktarıcı' olarak ayarlanmış ancak GenelAktarıcıAyarları eksik" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "İleti dışa aktarma görevi GenelAktarıcıAyarları.MüşteriTürü 'A9' ya da 'A10' olarak ayarlanmalıdır" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "İleti dışa aktarma görevinin GenelAktarıcıAyarları.E-postaAdresi ayarı geçerli bir e-posta adresi olmalıdır" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "İleti dışa aktarma görevi GenelAktarıcıAyarları.SmtpParolası ayarlanmalıdır" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "İleti dışa aktarma görevi GenelAktarıcıAyarları.SmtpKullanıcıAdı ayarlanmalıdır" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "İleti dışa aktarma görevinin GenelAktarıcıE-postaAdresi geçerli bir e-posta adresi olmalıdır" diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 43d3fde87..7f8fa4ba1 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - 测试邮箱设置" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "请求中图片为空" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v 已移出团队。" }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "请求中图片为空" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "无法解码团队图标" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "无法编码团队图标" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "获取团队时发生错误" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "请求中缺失图片文件" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "无法打开图像文件" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "不能解析混合表单" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "不能上传团队图标。没有配置图片存储。" + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "不能上传团队图标。文件太大。" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "无法保存团队图标" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "使用电子邮件注册团队被禁用。" @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "无法激活揭开的插件。插件可能已存在并已激活。" }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "保存插件状态到配置出错" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "消息导出任务 ExportFormat 必须为 'actiance' 或 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "消息导出任务 ExportFormat 必须为 'actiance' 或 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "消息导出任务 FileLocation 比如为可写的目录" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "消息导出任务 FileLocation 必须为 FileSettings.Directory 的子目录" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "消息导出任务 GlobalRelayEmailAddress 必须为有效的电子邮箱地址" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "消息导出任务 GlobalRelayEmailAddress 必须为有效的电子邮箱地址" diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index 0700a15a5..f78f6b288 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -107,6 +107,18 @@ "id": "api.admin.test_email.subject", "translation": "Mattermost - 測試電子郵件設定" }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket is required" + }, + { + "id": "api.admin.test_s3.missing_s3_endpoint", + "translation": "S3 Endpoint is required" + }, + { + "id": "api.admin.test_s3.missing_s3_region", + "translation": "S3 Region is required" + }, { "id": "api.admin.upload_brand_image.array.app_error", "translation": "要求的 'image' 欄位為空陣列" @@ -2330,6 +2342,50 @@ "id": "api.team.remove_user_from_team.removed", "translation": "%v 已從團隊中移除。" }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "要求的 'image' 欄位為空陣列" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Could not decode team icon" + }, + { + "id": "api.team.set_team_icon.decode_config.app_error", + "translation": "Could not decode team icon metadata" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Could not encode team icon" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "An error occurred getting the team" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "要求的 'image' 欄位沒有檔案" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "無法開啟圖片檔案" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "無法解析 multipart 表單" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "無法上傳檔案。尚未設定圖片儲存位置。" + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "無法上傳圖片檔案。檔案太大。" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Could not save team icon" + }, { "id": "api.team.signup_team.email_disabled.app_error", "translation": "使用電子郵件註冊團隊已被停用。" @@ -3646,6 +3702,10 @@ "id": "app.plugin.activate.app_error", "translation": "無法啟動已解開的模組。模組可能已存在並已啟動。" }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." + }, { "id": "app.plugin.config.app_error", "translation": "儲存模組狀態於設定時錯誤" @@ -4906,6 +4966,10 @@ "id": "model.config.is_valid.message_export.export_type.app_error", "translation": "訊息匯出工作的 ExportFormat 必須為 'actiance' 或 'globalrelay'" }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "訊息匯出工作的 ExportFormat 必須為 'actiance' 或 'globalrelay'" + }, { "id": "model.config.is_valid.message_export.file_location.app_error", "translation": "匯出訊息工作 FileLocation 設定必須為可寫入的目錄,匯出資料將寫入此處" @@ -4914,6 +4978,26 @@ "id": "model.config.is_valid.message_export.file_location.relative", "translation": "匯出訊息工作 FileLocation 設定必須為 FileSettings.Directory 的子目錄" }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Message export job ExportFormat is set to 'globalrelay', but GlobalRelaySettings are missing" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Message export GlobalRelaySettings.CustomerType must be set to one of either 'A9' or 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "訊息匯出工作的 GlobalRelayEmailAddress 必須為 'actiance' 或 'globalrelay'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpPassword must be set" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set" + }, { "id": "model.config.is_valid.message_export.global_relay_email_address.app_error", "translation": "訊息匯出工作的 GlobalRelayEmailAddress 必須為 'actiance' 或 'globalrelay'" -- cgit v1.2.3-1-g7c22 From 7e444dfd88d117df5b5a6ea589c233f71331e2ff Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 20 Mar 2018 13:19:34 -0500 Subject: add config for saml home realm discovery bypass (#8480) --- config/default.json | 2 ++ model/config.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/config/default.json b/config/default.json index b6bcc270a..9993740f6 100644 --- a/config/default.json +++ b/config/default.json @@ -282,6 +282,8 @@ "IdpUrl": "", "IdpDescriptorUrl": "", "AssertionConsumerServiceURL": "", + "ScopingIDPProviderId": "", + "ScopingIDPName": "", "IdpCertificateFile": "", "PublicCertificateFile": "", "PrivateKeyFile": "", diff --git a/model/config.go b/model/config.go index 98e331f10..cf26c4205 100644 --- a/model/config.go +++ b/model/config.go @@ -1291,6 +1291,9 @@ type SamlSettings struct { IdpDescriptorUrl *string AssertionConsumerServiceURL *string + ScopingIDPProviderId *string + ScopingIDPName *string + IdpCertificateFile *string PublicCertificateFile *string PrivateKeyFile *string -- cgit v1.2.3-1-g7c22 From b118451f3de4d678e35d3d6c7f188439d3e0add0 Mon Sep 17 00:00:00 2001 From: Jason Blais Date: Wed, 21 Mar 2018 09:11:51 -0400 Subject: Update plugin.go (#8486) --- app/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/plugin.go b/app/plugin.go index 6702e9227..903f4b767 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -401,7 +401,7 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) } else if err := sandbox.CheckSupport(); err != nil { l4g.Warn(err.Error()) - l4g.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server") + l4g.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server. See documentation to learn more: https://developers.mattermost.com/extend/plugins/security/") options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) } else { options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider)) -- cgit v1.2.3-1-g7c22 From 6ef6c6996842be5d408a4f9d6fcdabc323bf31c9 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Wed, 21 Mar 2018 09:13:20 -0400 Subject: Fixed missing error in log message (#8485) --- app/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/session.go b/app/session.go index 88f52477f..c9208f2b2 100644 --- a/app/session.go +++ b/app/session.go @@ -225,7 +225,7 @@ func (a *App) UpdateLastActivityAtIfNeeded(session model.Session) { } if result := <-a.Srv.Store.Session().UpdateLastActivityAt(session.Id, now); result.Err != nil { - l4g.Error(utils.T("api.status.last_activity.error"), session.UserId, session.Id) + l4g.Error(utils.T("api.status.last_activity.error"), session.UserId, session.Id, result.Err) } session.LastActivityAt = now -- cgit v1.2.3-1-g7c22 From b4784be2de6b841529e3dc3ec4c1ae1aade0121a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 21 Mar 2018 16:11:45 +0100 Subject: Receive the email date as parameter (#8484) --- utils/mail.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/mail.go b/utils/mail.go index c59406a18..2a2da9bf1 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -221,10 +221,10 @@ func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from mail.Address, subje return err } - return SendMail(c, mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, fileBackend) + return SendMail(c, mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, fileBackend, time.Now()) } -func SendMail(c *smtp.Client, mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, fileBackend FileBackend) *model.AppError { +func SendMail(c *smtp.Client, mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, fileBackend FileBackend, date time.Time) *model.AppError { l4g.Debug(T("utils.mail.send_mail.sending.debug"), mimeTo, subject) htmlMessage := "\r\n" + htmlBody + "" @@ -249,7 +249,7 @@ func SendMail(c *smtp.Client, mimeTo, smtpTo string, from mail.Address, subject, m := gomail.NewMessage(gomail.SetCharset("UTF-8")) m.SetHeaders(headers) - m.SetDateHeader("Date", time.Now()) + m.SetDateHeader("Date", date) m.SetBody("text/plain", txtBody) m.AddAlternative("text/html", htmlMessage) -- cgit v1.2.3-1-g7c22 From b1b23079c6a49df29b6f27b85e98d6a9b1d3607c Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Wed, 21 Mar 2018 11:33:47 -0400 Subject: Fix paging for GET /teams and GET /teams/{id}/members endpoints (#8488) --- api4/team.go | 6 +++--- api4/team_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/api4/team.go b/api4/team.go index 8e4c5c312..f8a1c556c 100644 --- a/api4/team.go +++ b/api4/team.go @@ -285,7 +285,7 @@ func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - if members, err := c.App.GetTeamMembers(c.Params.TeamId, c.Params.Page, c.Params.PerPage); err != nil { + if members, err := c.App.GetTeamMembers(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage); err != nil { c.Err = err return } else { @@ -543,9 +543,9 @@ func getAllTeams(c *Context, w http.ResponseWriter, r *http.Request) { var err *model.AppError if c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - teams, err = c.App.GetAllTeamsPage(c.Params.Page, c.Params.PerPage) + teams, err = c.App.GetAllTeamsPage(c.Params.Page*c.Params.PerPage, c.Params.PerPage) } else { - teams, err = c.App.GetAllOpenTeamsPage(c.Params.Page, c.Params.PerPage) + teams, err = c.App.GetAllOpenTeamsPage(c.Params.Page*c.Params.PerPage, c.Params.PerPage) } if err != nil { diff --git a/api4/team_test.go b/api4/team_test.go index 04a0e9ae4..c2edcdaaa 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -567,6 +567,10 @@ func TestGetAllTeams(t *testing.T) { _, resp := Client.CreateTeam(team) CheckNoError(t, resp) + team2 := &model.Team{DisplayName: "Name2", Name: GenerateTestTeamName(), Email: th.GenerateTestEmail(), Type: model.TEAM_OPEN, AllowOpenInvite: true} + _, resp = Client.CreateTeam(team2) + CheckNoError(t, resp) + rrteams, resp := Client.GetAllTeams("", 0, 1) CheckNoError(t, resp) @@ -611,6 +615,17 @@ func TestGetAllTeams(t *testing.T) { t.Fatal("wrong number of teams - should be 0") } + rrteams, resp = Client.GetAllTeams("", 0, 2) + CheckNoError(t, resp) + rrteams2, resp = Client.GetAllTeams("", 1, 2) + CheckNoError(t, resp) + + for _, t1 := range rrteams { + for _, t2 := range rrteams2 { + assert.NotEqual(t, t1.Id, t2.Id, "different pages should not have the same teams") + } + } + Client.Logout() _, resp = Client.GetAllTeams("", 1, 10) CheckUnauthorizedStatus(t, resp) @@ -1140,6 +1155,17 @@ func TestGetTeamMembers(t *testing.T) { t.Fatal("should be no member") } + rmembers, resp = Client.GetTeamMembers(team.Id, 0, 2, "") + CheckNoError(t, resp) + rmembers2, resp := Client.GetTeamMembers(team.Id, 1, 2, "") + CheckNoError(t, resp) + + for _, tm1 := range rmembers { + for _, tm2 := range rmembers2 { + assert.NotEqual(t, tm1.UserId+tm1.TeamId, tm2.UserId+tm2.TeamId, "different pages should not have the same members") + } + } + _, resp = Client.GetTeamMembers("junk", 0, 100, "") CheckBadRequestStatus(t, resp) -- cgit v1.2.3-1-g7c22 From 9d701c704416a1d8648dd2818a8a15c4da99b424 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 21 Mar 2018 14:27:14 -0400 Subject: Fix various segfaults when running `go test` manually (#8448) * failing to find i18n shouldn't segfault The server was trying to handle the fact that it couldn't find the i18n directory, by emitting a translated log message... * fix utils.FindDir The attempts to find the directory in the parent or grandparent directory don't work if the current working directory was inside `enterprise`, with `enterprise` itself being a symlink as per the usual developer setup. Recurse to the root of the filesystem, cleaning the path along the way to work around this limitation (and allow tests to be run from an arbitrarily deep nesting level.) Fix corresponding usages to employ filepath.Join. * failing to find html templates shouldn't segfault * fail fast if the test user cannot be created * rework utils.FindDir to retain backwards compatibility --- api/file_test.go | 3 ++- api/user_test.go | 3 ++- api4/apitestlib.go | 9 +++++++-- api4/oauth.go | 3 ++- api4/plugin_test.go | 3 ++- app/app.go | 6 +++++- app/auto_posts.go | 3 ++- app/import_test.go | 7 ++++--- app/saml.go | 3 +-- app/user.go | 3 ++- utils/config.go | 30 ++++++++++++------------------ utils/html.go | 10 ++++++++-- utils/i18n.go | 12 +++++++----- utils/license.go | 3 ++- web/web.go | 3 ++- 15 files changed, 60 insertions(+), 41 deletions(-) diff --git a/api/file_test.go b/api/file_test.go index 8e5fc6f67..7a04674cd 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "net/http" "os" + "path/filepath" "strings" "testing" "time" @@ -870,7 +871,7 @@ func TestGetInfoForFilename(t *testing.T) { func readTestFile(name string) ([]byte, error) { path, _ := utils.FindDir("tests") - file, err := os.Open(path + "/" + name) + file, err := os.Open(filepath.Join(path, name)) if err != nil { return nil, err } diff --git a/api/user_test.go b/api/user_test.go index f65d7c45b..518379305 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -12,6 +12,7 @@ import ( "mime/multipart" "net/http" "os" + "path/filepath" "strings" "testing" "time" @@ -778,7 +779,7 @@ func TestUserUploadProfileImage(t *testing.T) { } path, _ := utils.FindDir("tests") - file, err := os.Open(path + "/test.png") + file, err := os.Open(filepath.Join(path, "test.png")) if err != nil { t.Fatal(err) } diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 6edd37812..386afdadd 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "os" + "path/filepath" "reflect" "strconv" "strings" @@ -302,7 +303,11 @@ func (me *TestHelper) CreateUserWithClient(client *model.Client4) *model.User { } utils.DisableDebugLogForTest() - ruser, _ := client.CreateUser(user) + ruser, response := client.CreateUser(user) + if response.Error != nil { + panic(response.Error) + } + ruser.Password = "Password1" store.Must(me.App.Srv.Store.User().VerifyEmail(ruser.Id)) utils.EnableDebugLogForTest() @@ -675,7 +680,7 @@ func CheckInternalErrorStatus(t *testing.T, resp *model.Response) { func readTestFile(name string) ([]byte, error) { path, _ := utils.FindDir("tests") - file, err := os.Open(path + "/" + name) + file, err := os.Open(filepath.Join(path, name)) if err != nil { return nil, err } diff --git a/api4/oauth.go b/api4/oauth.go index d0f43256a..a173159b6 100644 --- a/api4/oauth.go +++ b/api4/oauth.go @@ -6,6 +6,7 @@ package api4 import ( "net/http" "net/url" + "path/filepath" "strings" l4g "github.com/alecthomas/log4go" @@ -375,7 +376,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public") staticDir, _ := utils.FindDir(model.CLIENT_DIR) - http.ServeFile(w, r, staticDir+"root.html") + http.ServeFile(w, r, filepath.Join(staticDir, "root.html")) } func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/plugin_test.go b/api4/plugin_test.go index e385b5c8c..045ae9212 100644 --- a/api4/plugin_test.go +++ b/api4/plugin_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io/ioutil" "os" + "path/filepath" "testing" "github.com/mattermost/mattermost-server/model" @@ -53,7 +54,7 @@ func TestPlugin(t *testing.T) { }() path, _ := utils.FindDir("tests") - file, err := os.Open(path + "/testplugin.tar.gz") + file, err := os.Open(filepath.Join(path, "testplugin.tar.gz")) if err != nil { t.Fatal(err) } diff --git a/app/app.go b/app/app.go index f5e5dd21e..48e95fb99 100644 --- a/app/app.go +++ b/app/app.go @@ -439,7 +439,11 @@ func (a *App) WaitForGoroutines() { } func (a *App) HTMLTemplates() *template.Template { - return a.htmlTemplateWatcher.Templates() + if a.htmlTemplateWatcher != nil { + return a.htmlTemplateWatcher.Templates() + } + + return nil } func (a *App) HTTPClient(trustURLs bool) *http.Client { diff --git a/app/auto_posts.go b/app/auto_posts.go index 379c74ab7..a2adb9d6c 100644 --- a/app/auto_posts.go +++ b/app/auto_posts.go @@ -7,6 +7,7 @@ import ( "bytes" "io" "os" + "path/filepath" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" @@ -43,7 +44,7 @@ func (cfg *AutoPostCreator) UploadTestFile() ([]string, bool) { filename := cfg.ImageFilenames[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.ImageFilenames) - 1})] path, _ := utils.FindDir("web/static/images") - file, err := os.Open(path + "/" + filename) + file, err := os.Open(filepath.Join(path, filename)) if err != nil { return nil, false } diff --git a/app/import_test.go b/app/import_test.go index 6a284f63d..f294c8731 100644 --- a/app/import_test.go +++ b/app/import_test.go @@ -4,6 +4,7 @@ package app import ( + "path/filepath" "runtime/debug" "strings" "testing" @@ -370,7 +371,7 @@ func TestImportValidateUserImportData(t *testing.T) { // Test a valid User with all fields populated. testsDir, _ := utils.FindDir("tests") data = UserImportData{ - ProfileImage: ptrStr(testsDir + "test.png"), + ProfileImage: ptrStr(filepath.Join(testsDir, "test.png")), Username: ptrStr("bob"), Email: ptrStr("bob@example.com"), AuthService: ptrStr("ldap"), @@ -1487,7 +1488,7 @@ func TestImportImportUser(t *testing.T) { username := model.NewId() testsDir, _ := utils.FindDir("tests") data = UserImportData{ - ProfileImage: ptrStr(testsDir + "test.png"), + ProfileImage: ptrStr(filepath.Join(testsDir, "test.png")), Username: &username, Email: ptrStr(model.NewId() + "@example.com"), Nickname: ptrStr(model.NewId()), @@ -1543,7 +1544,7 @@ func TestImportImportUser(t *testing.T) { // Alter all the fields of that user. data.Email = ptrStr(model.NewId() + "@example.com") - data.ProfileImage = ptrStr(testsDir + "testgif.gif") + data.ProfileImage = ptrStr(filepath.Join(testsDir, "testgif.gif")) data.AuthService = ptrStr("ldap") data.AuthData = &username data.Nickname = ptrStr(model.NewId()) diff --git a/app/saml.go b/app/saml.go index bd9f647a1..29d5f8772 100644 --- a/app/saml.go +++ b/app/saml.go @@ -8,7 +8,6 @@ import ( "mime/multipart" "net/http" "os" - "path/filepath" "github.com/mattermost/mattermost-server/model" @@ -42,7 +41,7 @@ func WriteSamlFile(fileData *multipart.FileHeader) *model.AppError { defer file.Close() configDir, _ := utils.FindDir("config") - out, err := os.Create(configDir + filename) + out, err := os.Create(filepath.Join(configDir, filename)) if err != nil { return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/app/user.go b/app/user.go index 7a6dc0b49..8f393038a 100644 --- a/app/user.go +++ b/app/user.go @@ -18,6 +18,7 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "path/filepath" "strconv" "strings" @@ -717,7 +718,7 @@ func CreateProfileImage(username string, userId string, initialFont string) ([]b initial := string(strings.ToUpper(username)[0]) fontDir, _ := utils.FindDir("fonts") - fontBytes, err := ioutil.ReadFile(fontDir + initialFont) + fontBytes, err := ioutil.ReadFile(filepath.Join(fontDir, initialFont)) if err != nil { return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/utils/config.go b/utils/config.go index 93a870743..5e9849930 100644 --- a/utils/config.go +++ b/utils/config.go @@ -9,7 +9,6 @@ import ( "io" "io/ioutil" "os" - "path" "path/filepath" "strconv" "strings" @@ -51,21 +50,17 @@ func FindConfigFile(fileName string) (path string) { return "" } +// FindDir looks for the given directory in nearby ancestors, falling back to `./` if not found. func FindDir(dir string) (string, bool) { - fileName := "." - found := false - if _, err := os.Stat("./" + dir + "/"); err == nil { - fileName, _ = filepath.Abs("./" + dir + "/") - found = true - } else if _, err := os.Stat("../" + dir + "/"); err == nil { - fileName, _ = filepath.Abs("../" + dir + "/") - found = true - } else if _, err := os.Stat("../../" + dir + "/"); err == nil { - fileName, _ = filepath.Abs("../../" + dir + "/") - found = true + for _, parent := range []string{".", "..", "../.."} { + foundDir, err := filepath.Abs(filepath.Join(parent, dir)) + if err != nil { + continue + } else if _, err := os.Stat(foundDir); err == nil { + return foundDir, true + } } - - return fileName + "/", found + return "./", false } func DisableDebugLogForTest() { @@ -136,11 +131,10 @@ func ConfigureLog(s *model.LogSettings) { func GetLogFileLocation(fileLocation string) string { if fileLocation == "" { - logDir, _ := FindDir("logs") - return logDir + LOG_FILENAME - } else { - return path.Join(fileLocation, LOG_FILENAME) + fileLocation, _ = FindDir("logs") } + + return filepath.Join(fileLocation, LOG_FILENAME) } func SaveConfig(fileName string, config *model.Config) *model.AppError { diff --git a/utils/html.go b/utils/html.go index 6bbe55c6d..f9a7abe5b 100644 --- a/utils/html.go +++ b/utils/html.go @@ -5,8 +5,10 @@ package utils import ( "bytes" + "errors" "html/template" "io" + "path/filepath" "reflect" "sync/atomic" @@ -39,7 +41,7 @@ func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) { return nil, err } - if htmlTemplates, err := template.ParseGlob(templatesDir + "*.html"); err != nil { + if htmlTemplates, err := template.ParseGlob(filepath.Join(templatesDir, "*.html")); err != nil { return nil, err } else { ret.templates.Store(htmlTemplates) @@ -56,7 +58,7 @@ func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) { case event := <-watcher.Events: if event.Op&fsnotify.Write == fsnotify.Write { l4g.Info("Re-parsing templates because of modified file %v", event.Name) - if htmlTemplates, err := template.ParseGlob(templatesDir + "*.html"); err != nil { + if htmlTemplates, err := template.ParseGlob(filepath.Join(templatesDir, "*.html")); err != nil { l4g.Error("Failed to parse templates %v", err) } else { ret.templates.Store(htmlTemplates) @@ -103,6 +105,10 @@ func (t *HTMLTemplate) Render() string { } func (t *HTMLTemplate) RenderToWriter(w io.Writer) error { + if t.Templates == nil { + return errors.New("no html templates") + } + if err := t.Templates.ExecuteTemplate(w, t.TemplateName, t); err != nil { l4g.Error(T("api.api.render.error"), t.TemplateName, err) return err diff --git a/utils/i18n.go b/utils/i18n.go index 8ed82d19f..7b8d1fef0 100644 --- a/utils/i18n.go +++ b/utils/i18n.go @@ -23,13 +23,15 @@ var settings model.LocalizationSettings // this functions loads translations from filesystem // and assign english while loading server config func TranslationsPreInit() error { + // Set T even if we fail to load the translations. Lots of shutdown handling code will + // segfault trying to handle the error, and the untranslated IDs are strictly better. + T = TfuncWithFallback("en") + TDefault = TfuncWithFallback("en") + if err := InitTranslationsWithDir("i18n"); err != nil { return err } - T = TfuncWithFallback("en") - TDefault = TfuncWithFallback("en") - return nil } @@ -51,9 +53,9 @@ func InitTranslationsWithDir(dir string) error { for _, f := range files { if filepath.Ext(f.Name()) == ".json" { filename := f.Name() - locales[strings.Split(filename, ".")[0]] = i18nDirectory + filename + locales[strings.Split(filename, ".")[0]] = filepath.Join(i18nDirectory, filename) - if err := i18n.LoadTranslationFile(i18nDirectory + filename); err != nil { + if err := i18n.LoadTranslationFile(filepath.Join(i18nDirectory, filename)); err != nil { return err } } diff --git a/utils/license.go b/utils/license.go index 2853a58d0..cf874b62b 100644 --- a/utils/license.go +++ b/utils/license.go @@ -12,6 +12,7 @@ import ( "encoding/pem" "io/ioutil" "os" + "path/filepath" "strconv" "strings" @@ -114,7 +115,7 @@ func GetLicenseFileFromDisk(fileName string) []byte { func GetLicenseFileLocation(fileLocation string) string { if fileLocation == "" { configDir, _ := FindDir("config") - return configDir + "mattermost.mattermost-license" + return filepath.Join(configDir, "mattermost.mattermost-license") } else { return fileLocation } diff --git a/web/web.go b/web/web.go index 22fe43923..c9d318397 100644 --- a/web/web.go +++ b/web/web.go @@ -5,6 +5,7 @@ package web import ( "net/http" + "path/filepath" "strings" "github.com/NYTimes/gziphandler" @@ -102,5 +103,5 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public") staticDir, _ := utils.FindDir(model.CLIENT_DIR) - http.ServeFile(w, r, staticDir+"root.html") + http.ServeFile(w, r, filepath.Join(staticDir, "root.html")) } -- cgit v1.2.3-1-g7c22 From 74e703f58daff2aded07a969410f988cff9ada9b Mon Sep 17 00:00:00 2001 From: Chris Duarte Date: Thu, 22 Mar 2018 06:53:43 -0700 Subject: Timezone feature (#8185) * Add supported timezones into config Remove Timezone list creation in favor of timezone from configs Add Timezone field to Users table Clean up format of SupportedTimezones in config * Remove unwanted change * Add test for updating user timezone * Add empty map[string]string if Timezone is null * Add EnableTimezoneSelection config * Revert back to map[string]string for ClientConfig * Refactor SupportedTimezones into timezones.json * Include timezones.json in TestConfigFlag * Add timezone api endpoint * Bump varchar size to 256 and setMaxSize in user_store * Refactor LoadConfig to LoadConfig and LoadTimezoneConfig * Remove unnecessary argument in LoadConfig, mail_test * Add test for timezone endpoint * Add license header * Refactor timezones endpoint to system.go * Add system base route to timezone endpoint * db timezone upgrade in db v4.9 * Avoid saving SupportedTimezones to config.json * Add timezonePath support in config * Remove EnableTimezoneSelection from config * Use return statement without return parameter * Refactor test for SupportedTimezones * Check for supportedTimezone != nil instead of using len * Decouple SupportedTimezones out of Config * Fix failing test * Add LastTeamIconUpdate back in upgrade.go * Write timezone config in config_flag_test * Add code fallback for default timezone support --- api4/system.go | 14 + api4/system_test.go | 12 + api4/user_test.go | 13 + app/app.go | 5 + app/timezone.go | 28 ++ cmd/commands/config_flag_test.go | 6 + config/default.json | 3 + config/timezones.json | 596 +++++++++++++++++++++++++++++++++++++ i18n/en.json | 12 + model/client4.go | 16 + model/config.go | 12 + model/timezone.go | 628 +++++++++++++++++++++++++++++++++++++++ model/user.go | 10 + store/sqlstore/upgrade.go | 7 + store/sqlstore/user_store.go | 1 + utils/config_test.go | 9 + utils/timezone.go | 25 ++ 17 files changed, 1397 insertions(+) create mode 100644 app/timezone.go create mode 100644 config/timezones.json create mode 100644 model/timezone.go create mode 100644 utils/timezone.go diff --git a/api4/system.go b/api4/system.go index 7b63afc0b..4ae8ee7b9 100644 --- a/api4/system.go +++ b/api4/system.go @@ -17,6 +17,8 @@ import ( func (api *API) InitSystem() { api.BaseRoutes.System.Handle("/ping", api.ApiHandler(getSystemPing)).Methods("GET") + api.BaseRoutes.System.Handle("/timezones", api.ApiSessionRequired(getSupportedTimezones)).Methods("GET") + api.BaseRoutes.ApiRoot.Handle("/config", api.ApiSessionRequired(getConfig)).Methods("GET") api.BaseRoutes.ApiRoot.Handle("/config", api.ApiSessionRequired(updateConfig)).Methods("PUT") api.BaseRoutes.ApiRoot.Handle("/config/reload", api.ApiSessionRequired(configReload)).Methods("POST") @@ -378,6 +380,18 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(rows.ToJson())) } +func getSupportedTimezones(c *Context, w http.ResponseWriter, r *http.Request) { + supportedTimezones := c.App.Timezones() + + if supportedTimezones != nil { + w.Write([]byte(model.TimezonesToJson(supportedTimezones))) + return + } + + emptyTimezones := make([]string, 0) + w.Write([]byte(model.TimezonesToJson(emptyTimezones))) +} + func testS3(c *Context, w http.ResponseWriter, r *http.Request) { cfg := model.ConfigFromJson(r.Body) if cfg == nil { diff --git a/api4/system_test.go b/api4/system_test.go index 6ef02cbfe..bb3790d4b 100644 --- a/api4/system_test.go +++ b/api4/system_test.go @@ -527,3 +527,15 @@ func TestS3TestConnection(t *testing.T) { t.Fatal("should return error ") } } + +func TestSupportedTimezones(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + Client := th.Client + + supportedTimezonesFromConfig := th.App.Timezones() + supportedTimezones, resp := Client.GetSupportedTimezone() + + CheckNoError(t, resp) + assert.Equal(t, supportedTimezonesFromConfig, supportedTimezones) +} diff --git a/api4/user_test.go b/api4/user_test.go index f04cd6ab2..359756aeb 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -996,6 +996,10 @@ func TestPatchUser(t *testing.T) { patch.Position = new(string) patch.NotifyProps = model.StringMap{} patch.NotifyProps["comment"] = "somethingrandom" + patch.Timezone = model.StringMap{} + patch.Timezone["useAutomaticTimezone"] = "true" + patch.Timezone["automaticTimezone"] = "America/New_York" + patch.Timezone["manualTimezone"] = "" ruser, resp := Client.PatchUser(user.Id, patch) CheckNoError(t, resp) @@ -1019,6 +1023,15 @@ func TestPatchUser(t *testing.T) { if ruser.NotifyProps["comment"] != "somethingrandom" { t.Fatal("NotifyProps did not update properly") } + if ruser.Timezone["useAutomaticTimezone"] != "true" { + t.Fatal("useAutomaticTimezone did not update properly") + } + if ruser.Timezone["automaticTimezone"] != "America/New_York" { + t.Fatal("automaticTimezone did not update properly") + } + if ruser.Timezone["manualTimezone"] != "" { + t.Fatal("manualTimezone did not update properly") + } patch.Username = model.NewString(th.BasicUser2.Username) _, resp = Client.PatchUser(user.Id, patch) diff --git a/app/app.go b/app/app.go index 48e95fb99..6329a80d3 100644 --- a/app/app.go +++ b/app/app.go @@ -63,6 +63,8 @@ type App struct { clientLicenseValue atomic.Value licenseListeners map[string]func() + timezones atomic.Value + siteURL string newStore func() store.Store @@ -125,6 +127,9 @@ func New(options ...Option) (outApp *App, outErr error) { return nil, err } app.EnableConfigWatch() + + app.LoadTimezones() + if err := utils.InitTranslations(app.Config().LocalizationSettings); err != nil { return nil, errors.Wrapf(err, "unable to load Mattermost translation files") } diff --git a/app/timezone.go b/app/timezone.go new file mode 100644 index 000000000..84d912da6 --- /dev/null +++ b/app/timezone.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" +) + +func (a *App) Timezones() model.SupportedTimezones { + if cfg := a.timezones.Load(); cfg != nil { + return cfg.(model.SupportedTimezones) + } + return model.SupportedTimezones{} +} + +func (a *App) LoadTimezones() { + timezonePath := "timezones.json" + + if a.Config().TimezoneSettings.SupportedTimezonesPath != nil && len(*a.Config().TimezoneSettings.SupportedTimezonesPath) > 0 { + timezonePath = *a.Config().TimezoneSettings.SupportedTimezonesPath + } + + timezoneCfg := utils.LoadTimezones(timezonePath) + + a.timezones.Store(timezoneCfg) +} diff --git a/cmd/commands/config_flag_test.go b/cmd/commands/config_flag_test.go index 8d284ab73..7ea0d5153 100644 --- a/cmd/commands/config_flag_test.go +++ b/cmd/commands/config_flag_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" + "encoding/json" "github.com/mattermost/mattermost-server/cmd" "github.com/mattermost/mattermost-server/utils" ) @@ -26,6 +27,11 @@ func TestConfigFlag(t *testing.T) { configPath := filepath.Join(dir, "foo.json") require.NoError(t, ioutil.WriteFile(configPath, []byte(config.ToJson()), 0600)) + timezones := utils.LoadTimezones("timezones.json") + tzConfigPath := filepath.Join(dir, "timezones.json") + timezoneData, _ := json.Marshal(timezones) + require.NoError(t, ioutil.WriteFile(tzConfigPath, timezoneData, 0600)) + i18n, ok := utils.FindDir("i18n") require.True(t, ok) require.NoError(t, utils.CopyDir(i18n, filepath.Join(dir, "i18n"))) diff --git a/config/default.json b/config/default.json index 9993740f6..908604d7e 100644 --- a/config/default.json +++ b/config/default.json @@ -211,6 +211,9 @@ "AllowCustomThemes": true, "AllowedThemes": [] }, + "TimezoneSettings": { + "SupportedTimezonesPath": "timezones.json" + }, "GitLabSettings": { "Enable": false, "Secret": "", diff --git a/config/timezones.json b/config/timezones.json new file mode 100644 index 000000000..146097134 --- /dev/null +++ b/config/timezones.json @@ -0,0 +1,596 @@ +[ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/East-Saskatchewan", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Pacific-New", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" +] diff --git a/i18n/en.json b/i18n/en.json index 42650a868..974334d85 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3770,6 +3770,18 @@ "id": "app.team.join_user_to_team.max_accounts.app_error", "translation": "This team has reached the maximum number of allowed accounts. Contact your systems administrator to set a higher limit." }, + { + "id": "app.timezones.failed_deserialize.app_error", + "translation": "Failed to deserialize Timezone config file={{.Filename}}, err={{.Error}}" + }, + { + "id": "app.timezones.load_config.app_error", + "translation": "Timezone config file does not exists file={{.Filename}}" + }, + { + "id": "app.timezones.read_config.app_error", + "translation": "Failed to read Timezone config file={{.Filename}}, err={{.Error}}" + }, { "id": "app.user_access_token.disabled", "translation": "Personal access tokens are disabled on this server. Please contact your system administrator for details." diff --git a/model/client4.go b/model/client4.go index e7fe2bba0..3bbf7b4cc 100644 --- a/model/client4.go +++ b/model/client4.go @@ -314,6 +314,10 @@ func (c *Client4) GetAnalyticsRoute() string { return fmt.Sprintf("/analytics") } +func (c *Client4) GetTimezonesRoute() string { + return fmt.Sprintf(c.GetSystemRoute() + "/timezones") +} + func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) { return c.DoApiRequest(http.MethodGet, c.ApiUrl+url, "", etag) } @@ -3169,6 +3173,18 @@ func (c *Client4) DeleteReaction(reaction *Reaction) (bool, *Response) { } } +// Timezone Section + +// GetSupportedTimezone returns a page of supported timezones on the system. +func (c *Client4) GetSupportedTimezone() (SupportedTimezones, *Response) { + if r, err := c.DoApiGet(c.GetTimezonesRoute(), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return TimezonesFromJson(r.Body), BuildResponse(r) + } +} + // Open Graph Metadata Section // OpenGraph return the open graph metadata for a particular url if the site have the metadata diff --git a/model/config.go b/model/config.go index cf26c4205..4e13ca16e 100644 --- a/model/config.go +++ b/model/config.go @@ -1700,6 +1700,16 @@ func (s *MessageExportSettings) SetDefaults() { } } +type TimezoneSettings struct { + SupportedTimezonesPath *string +} + +func (s *TimezoneSettings) SetDefaults() { + if s.SupportedTimezonesPath == nil { + s.SupportedTimezonesPath = NewString("timezones.json") + } +} + type ConfigFunc func() *Config type Config struct { @@ -1733,6 +1743,7 @@ type Config struct { MessageExportSettings MessageExportSettings JobSettings JobSettings PluginSettings PluginSettings + TimezoneSettings TimezoneSettings } func (o *Config) Clone() *Config { @@ -1802,6 +1813,7 @@ func (o *Config) SetDefaults() { o.JobSettings.SetDefaults() o.WebrtcSettings.SetDefaults() o.MessageExportSettings.SetDefaults() + o.TimezoneSettings.SetDefaults() } func (o *Config) IsValid() *AppError { diff --git a/model/timezone.go b/model/timezone.go new file mode 100644 index 000000000..420b9d2e2 --- /dev/null +++ b/model/timezone.go @@ -0,0 +1,628 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SupportedTimezones []string + +func TimezonesToJson(timezoneList []string) string { + b, _ := json.Marshal(timezoneList) + return string(b) +} + +func TimezonesFromJson(data io.Reader) SupportedTimezones { + var timezones SupportedTimezones + json.NewDecoder(data).Decode(&timezones) + return timezones +} + +func DefaultUserTimezone() map[string]string { + defaultTimezone := make(map[string]string) + defaultTimezone["useAutomaticTimezone"] = "true" + defaultTimezone["automaticTimezone"] = "" + defaultTimezone["manualTimezone"] = "" + + return defaultTimezone +} + +var DefaultSupportedTimezones = []string{ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/East-Saskatchewan", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Pacific-New", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu", +} diff --git a/model/user.go b/model/user.go index f64275c83..0f215b0e7 100644 --- a/model/user.go +++ b/model/user.go @@ -71,6 +71,7 @@ type User struct { LastPictureUpdate int64 `json:"last_picture_update,omitempty"` FailedAttempts int `json:"failed_attempts,omitempty"` Locale string `json:"locale"` + Timezone StringMap `json:"timezone"` MfaActive bool `json:"mfa_active,omitempty"` MfaSecret string `json:"mfa_secret,omitempty"` LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` @@ -86,6 +87,7 @@ type UserPatch struct { Props StringMap `json:"props,omitempty"` NotifyProps StringMap `json:"notify_props,omitempty"` Locale *string `json:"locale"` + Timezone StringMap `json:"timezone"` } type UserAuth struct { @@ -208,6 +210,10 @@ func (u *User) PreSave() { u.SetDefaultNotifications() } + if u.Timezone == nil { + u.Props = make(map[string]string) + } + if len(u.Password) > 0 { u.Password = HashPassword(u.Password) } @@ -302,6 +308,10 @@ func (u *User) Patch(patch *UserPatch) { if patch.Locale != nil { u.Locale = *patch.Locale } + + if patch.Timezone != nil { + u.Timezone = patch.Timezone + } } // ToJson convert a User to a json string diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 58de85c7f..d528e464d 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -4,6 +4,7 @@ package sqlstore import ( + "encoding/json" "os" "strings" "time" @@ -376,6 +377,12 @@ func UpgradeDatabaseToVersion49(sqlStore SqlStore) { //TODO: Uncomment the following condition when version 4.9.0 is released //if shouldPerformUpgrade(sqlStore, VERSION_4_8_0, VERSION_4_9_0) { sqlStore.CreateColumnIfNotExists("Teams", "LastTeamIconUpdate", "bigint", "bigint", "0") + defaultTimezone := model.DefaultUserTimezone() + defaultTimezoneValue, err := json.Marshal(defaultTimezone) + if err != nil { + l4g.Critical(err) + } + sqlStore.CreateColumnIfNotExists("Users", "Timezone", "varchar(256)", "varchar(256)", string(defaultTimezoneValue)) // saveSchemaVersion(sqlStore, VERSION_4_9_0) //} } diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 5e84af930..f4ed3e400 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -79,6 +79,7 @@ func NewSqlUserStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) st table.ColMap("Locale").SetMaxSize(5) table.ColMap("MfaSecret").SetMaxSize(128) table.ColMap("Position").SetMaxSize(128) + table.ColMap("Timezone").SetMaxSize(256) } return us diff --git a/utils/config_test.go b/utils/config_test.go index 5809422f1..6d6181a3d 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -21,6 +21,15 @@ func TestConfig(t *testing.T) { InitTranslations(cfg.LocalizationSettings) } +func TestTimezoneConfig(t *testing.T) { + TranslationsPreInit() + supportedTimezones := LoadTimezones("timezones.json") + assert.Equal(t, len(supportedTimezones) > 0, true) + + supportedTimezones2 := LoadTimezones("timezones_file_does_not_exists.json") + assert.Equal(t, len(supportedTimezones2) > 0, true) +} + func TestFindConfigFile(t *testing.T) { dir, err := ioutil.TempDir("", "") require.NoError(t, err) diff --git a/utils/timezone.go b/utils/timezone.go new file mode 100644 index 000000000..ea5f15140 --- /dev/null +++ b/utils/timezone.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "encoding/json" + "io/ioutil" + + "github.com/mattermost/mattermost-server/model" +) + +func LoadTimezones(fileName string) model.SupportedTimezones { + var supportedTimezones model.SupportedTimezones + + if timezoneFile := FindConfigFile(fileName); timezoneFile == "" { + return model.DefaultSupportedTimezones + } else if raw, err := ioutil.ReadFile(timezoneFile); err != nil { + return model.DefaultSupportedTimezones + } else if err := json.Unmarshal(raw, &supportedTimezones); err != nil { + return model.DefaultSupportedTimezones + } else { + return supportedTimezones + } +} -- cgit v1.2.3-1-g7c22 From 6036ddad8598cc69511a259ffdafcce570653717 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Thu, 22 Mar 2018 10:57:29 -0400 Subject: MM-9804: emit defaults for all enterprise config (#8490) * MM-9804: emit defaults for all enterprise config This prevents the client from having to write inverted checks if a certain value defaults as `'true'` instead of `'false'`. * move EnableMobileFile(Upload|Download) to a defaulted enterprise configuration --- utils/config.go | 51 ++++++++++++++++++++++++- utils/config_test.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 148 insertions(+), 8 deletions(-) diff --git a/utils/config.go b/utils/config.go index 5e9849930..163838a13 100644 --- a/utils/config.go +++ b/utils/config.go @@ -408,8 +408,6 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L props["SupportEmail"] = *c.SupportSettings.SupportEmail props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments) - props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload) - props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload) props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink) props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort) @@ -443,8 +441,55 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L hasImageProxy := c.ServiceSettings.ImageProxyType != nil && *c.ServiceSettings.ImageProxyType != "" && c.ServiceSettings.ImageProxyURL != nil && *c.ServiceSettings.ImageProxyURL != "" props["HasImageProxy"] = strconv.FormatBool(hasImageProxy) + // Set default values for all options that require a license. + props["ExperimentalTownSquareIsReadOnly"] = "false" + props["ExperimentalEnableAuthenticationTransfer"] = "true" + props["EnableCustomBrand"] = "false" + props["CustomBrandText"] = "" + props["CustomDescriptionText"] = "" + props["EnableLdap"] = "false" + props["LdapLoginFieldName"] = "" + props["LdapNicknameAttributeSet"] = "false" + props["LdapFirstNameAttributeSet"] = "false" + props["LdapLastNameAttributeSet"] = "false" + props["LdapLoginButtonColor"] = "" + props["LdapLoginButtonBorderColor"] = "" + props["LdapLoginButtonTextColor"] = "" + props["EnableMultifactorAuthentication"] = "false" + props["EnforceMultifactorAuthentication"] = "false" + props["EnableCompliance"] = "false" + props["EnableMobileFileDownload"] = "true" + props["EnableMobileFileUpload"] = "true" + props["EnableSaml"] = "false" + props["SamlLoginButtonText"] = "" + props["SamlFirstNameAttributeSet"] = "false" + props["SamlLastNameAttributeSet"] = "false" + props["SamlNicknameAttributeSet"] = "false" + props["SamlLoginButtonColor"] = "" + props["SamlLoginButtonBorderColor"] = "" + props["SamlLoginButtonTextColor"] = "" + props["EnableCluster"] = "false" + props["EnableMetrics"] = "false" + props["EnableSignUpWithGoogle"] = "false" + props["EnableSignUpWithOffice365"] = "false" + props["PasswordMinimumLength"] = "0" + props["PasswordRequireLowercase"] = "false" + props["PasswordRequireUppercase"] = "false" + props["PasswordRequireNumber"] = "false" + props["PasswordRequireSymbol"] = "false" + props["EnableBanner"] = "false" + props["BannerText"] = "" + props["BannerColor"] = "" + props["BannerTextColor"] = "" + props["AllowBannerDismissal"] = "false" props["EnableThemeSelection"] = "true" + props["DefaultTheme"] = "" props["AllowCustomThemes"] = "true" + props["AllowedThemes"] = "" + props["DataRetentionEnableMessageDeletion"] = "false" + props["DataRetentionMessageRetentionDays"] = "0" + props["DataRetentionEnableFileDeletion"] = "false" + props["DataRetentionFileRetentionDays"] = "0" if license != nil { props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly) @@ -474,6 +519,8 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L if *license.Features.Compliance { props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable) + props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload) + props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload) } if *license.Features.SAML { diff --git a/utils/config_test.go b/utils/config_test.go index 6d6181a3d..f816e2ee8 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -12,6 +12,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" ) func TestConfig(t *testing.T) { @@ -202,14 +204,97 @@ func TestValidateLocales(t *testing.T) { } func TestGetClientConfig(t *testing.T) { - TranslationsPreInit() - cfg, _, err := LoadConfig("config.json") - require.Nil(t, err) + t.Parallel() + testCases := []struct { + description string + config *model.Config + diagnosticId string + license *model.License + expectedFields map[string]string + }{ + { + "unlicensed", + &model.Config{ + EmailSettings: model.EmailSettings{ + EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL), + }, + ThemeSettings: model.ThemeSettings{ + // Ignored, since not licensed. + AllowCustomThemes: bToP(false), + }, + }, + "", + nil, + map[string]string{ + "DiagnosticId": "", + "EmailNotificationContentsType": "full", + "AllowCustomThemes": "true", + }, + }, + { + "licensed, but not for theme management", + &model.Config{ + EmailSettings: model.EmailSettings{ + EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL), + }, + ThemeSettings: model.ThemeSettings{ + // Ignored, since not licensed. + AllowCustomThemes: bToP(false), + }, + }, + "tag1", + &model.License{ + Features: &model.Features{ + ThemeManagement: bToP(false), + }, + }, + map[string]string{ + "DiagnosticId": "tag1", + "EmailNotificationContentsType": "full", + "AllowCustomThemes": "true", + }, + }, + { + "licensed for theme management", + &model.Config{ + EmailSettings: model.EmailSettings{ + EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL), + }, + ThemeSettings: model.ThemeSettings{ + AllowCustomThemes: bToP(false), + }, + }, + "tag2", + &model.License{ + Features: &model.Features{ + ThemeManagement: bToP(true), + }, + }, + map[string]string{ + "DiagnosticId": "tag2", + "EmailNotificationContentsType": "full", + "AllowCustomThemes": "false", + }, + }, + } - configMap := GenerateClientConfig(cfg, "", nil) - if configMap["EmailNotificationContentsType"] != *cfg.EmailSettings.EmailNotificationContentsType { - t.Fatal("EmailSettings.EmailNotificationContentsType not exposed to client config") + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + testCase.config.SetDefaults() + if testCase.license != nil { + testCase.license.Features.SetDefaults() + } + + configMap := GenerateClientConfig(testCase.config, testCase.diagnosticId, testCase.license) + for expectedField, expectedValue := range testCase.expectedFields { + assert.Equal(t, expectedValue, configMap[expectedField]) + } + }) } + } func TestReadConfig(t *testing.T) { @@ -222,3 +307,11 @@ func TestReadConfig(t *testing.T) { assert.Equal(t, "http://foo.bar", *config.ServiceSettings.SiteURL) } + +func sToP(s string) *string { + return &s +} + +func bToP(b bool) *bool { + return &b +} -- cgit v1.2.3-1-g7c22 From c9e9484150fcc05ca12388c6db8b831c41f227eb Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 22 Mar 2018 19:46:30 +0200 Subject: MM-9853 Fix Team invite does not carry through SAML login (#8495) --- api/user.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/user.go b/api/user.go index 14cc881dc..560d722a4 100644 --- a/api/user.go +++ b/api/user.go @@ -1170,7 +1170,11 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { teamId := relayProps["team_id"] if len(teamId) > 0 { c.App.Go(func() { - c.App.AddDirectChannels(teamId, user) + if err := c.App.AddUserToTeamByTeamId(teamId, user); err != nil { + l4g.Error(err.Error()) + } else { + c.App.AddDirectChannels(teamId, user) + } }) } case model.OAUTH_ACTION_EMAIL_TO_SSO: -- cgit v1.2.3-1-g7c22 From 87762ae62eb887dfb3fd0957040919aede46f7d4 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Thu, 22 Mar 2018 15:33:02 -0400 Subject: Improved bulkload error handling (#8491) * log the config file path used by the server on startup * return an err if the bulk import command fails * log the underlying errors that occur when importing The code assumed all errors meant a missing resource, but it's possible something else is at fault. Including the error helps pinpoint that more readily. --- app/config.go | 1 + app/import.go | 36 ++++++++++++++++++------------------ cmd/commands/import.go | 1 + 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/config.go b/app/config.go index 460d580d8..ccd7236a0 100644 --- a/app/config.go +++ b/app/config.go @@ -54,6 +54,7 @@ func (a *App) LoadConfig(configFile string) *model.AppError { a.configFile = configPath utils.ConfigureLog(&cfg.LogSettings) + l4g.Info("Using config file at %s", configPath) a.config.Store(cfg) diff --git a/app/import.go b/app/import.go index 5a3158fab..e2e3aa1b7 100644 --- a/app/import.go +++ b/app/import.go @@ -399,7 +399,7 @@ func (a *App) ImportChannel(data *ChannelImportData, dryRun bool) *model.AppErro var team *model.Team if result := <-a.Srv.Store.Team().GetByName(*data.Team); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_channel.team_not_found.error", map[string]interface{}{"TeamName": *data.Team}, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_channel.team_not_found.error", map[string]interface{}{"TeamName": *data.Team}, result.Err.Error(), http.StatusBadRequest) } else { team = result.Data.(*model.Team) } @@ -781,7 +781,7 @@ func (a *App) ImportUser(data *UserImportData, dryRun bool) *model.AppError { if len(preferences) > 0 { if result := <-a.Srv.Store.Preference().Save(&preferences); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_user.save_preferences.error", nil, "", http.StatusInternalServerError) + return model.NewAppError("BulkImport", "app.import.import_user.save_preferences.error", nil, result.Err.Error(), http.StatusInternalServerError) } } @@ -899,7 +899,7 @@ func (a *App) ImportUserChannels(user *model.User, team *model.Team, teamMember if len(preferences) > 0 { if result := <-a.Srv.Store.Preference().Save(&preferences); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_user_channels.save_preferences.error", nil, "", http.StatusInternalServerError) + return model.NewAppError("BulkImport", "app.import.import_user_channels.save_preferences.error", nil, result.Err.Error(), http.StatusInternalServerError) } } @@ -1069,7 +1069,7 @@ func (a *App) ImportReaction(data *ReactionImportData, post *model.Post, dryRun var user *model.User if result := <-a.Srv.Store.User().GetByUsername(*data.User); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": data.User}, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": data.User}, result.Err.Error(), http.StatusBadRequest) } else { user = result.Data.(*model.User) } @@ -1092,7 +1092,7 @@ func (a *App) ImportReply(data *ReplyImportData, post *model.Post, dryRun bool) var user *model.User if result := <-a.Srv.Store.User().GetByUsername(*data.User); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": data.User}, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": data.User}, result.Err.Error(), http.StatusBadRequest) } else { user = result.Data.(*model.User) } @@ -1147,21 +1147,21 @@ func (a *App) ImportPost(data *PostImportData, dryRun bool) *model.AppError { var team *model.Team if result := <-a.Srv.Store.Team().GetByName(*data.Team); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_post.team_not_found.error", map[string]interface{}{"TeamName": *data.Team}, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_post.team_not_found.error", map[string]interface{}{"TeamName": *data.Team}, result.Err.Error(), http.StatusBadRequest) } else { team = result.Data.(*model.Team) } var channel *model.Channel if result := <-a.Srv.Store.Channel().GetByName(team.Id, *data.Channel, false); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_post.channel_not_found.error", map[string]interface{}{"ChannelName": *data.Channel}, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_post.channel_not_found.error", map[string]interface{}{"ChannelName": *data.Channel}, result.Err.Error(), http.StatusBadRequest) } else { channel = result.Data.(*model.Channel) } var user *model.User if result := <-a.Srv.Store.User().GetByUsername(*data.User); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": *data.User}, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": *data.User}, result.Err.Error(), http.StatusBadRequest) } else { user = result.Data.(*model.User) } @@ -1210,7 +1210,7 @@ func (a *App) ImportPost(data *PostImportData, dryRun bool) *model.AppError { var user *model.User if result := <-a.Srv.Store.User().GetByUsername(username); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": username}, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": username}, result.Err.Error(), http.StatusBadRequest) } else { user = result.Data.(*model.User) } @@ -1225,7 +1225,7 @@ func (a *App) ImportPost(data *PostImportData, dryRun bool) *model.AppError { if len(preferences) > 0 { if result := <-a.Srv.Store.Preference().Save(&preferences); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError) + return model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, result.Err.Error(), http.StatusInternalServerError) } } } @@ -1351,7 +1351,7 @@ func (a *App) ImportDirectChannel(data *DirectChannelImportData, dryRun bool) *m userIds = append(userIds, user.Id) userMap[username] = user.Id } else { - return model.NewAppError("BulkImport", "app.import.import_direct_channel.member_not_found.error", nil, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_direct_channel.member_not_found.error", nil, result.Err.Error(), http.StatusBadRequest) } } @@ -1360,14 +1360,14 @@ func (a *App) ImportDirectChannel(data *DirectChannelImportData, dryRun bool) *m if len(userIds) == 2 { ch, err := a.createDirectChannel(userIds[0], userIds[1]) if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { - return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_direct_channel.error", nil, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_direct_channel.error", nil, err.Error(), http.StatusBadRequest) } else { channel = ch } } else { ch, err := a.createGroupChannel(userIds, userIds[0]) if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { - return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, err.Error(), http.StatusBadRequest) } else { channel = ch } @@ -1403,7 +1403,7 @@ func (a *App) ImportDirectChannel(data *DirectChannelImportData, dryRun bool) *m if data.Header != nil { channel.Header = *data.Header if result := <-a.Srv.Store.Channel().Update(channel); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_direct_channel.update_header_failed.error", nil, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_direct_channel.update_header_failed.error", nil, result.Err.Error(), http.StatusBadRequest) } } @@ -1461,7 +1461,7 @@ func (a *App) ImportDirectPost(data *DirectPostImportData, dryRun bool) *model.A user := result.Data.(*model.User) userIds = append(userIds, user.Id) } else { - return model.NewAppError("BulkImport", "app.import.import_direct_post.channel_member_not_found.error", nil, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_direct_post.channel_member_not_found.error", nil, result.Err.Error(), http.StatusBadRequest) } } @@ -1469,14 +1469,14 @@ func (a *App) ImportDirectPost(data *DirectPostImportData, dryRun bool) *model.A if len(userIds) == 2 { ch, err := a.createDirectChannel(userIds[0], userIds[1]) if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { - return model.NewAppError("BulkImport", "app.import.import_direct_post.create_direct_channel.error", nil, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_direct_post.create_direct_channel.error", nil, err.Error(), http.StatusBadRequest) } else { channel = ch } } else { ch, err := a.createGroupChannel(userIds, userIds[0]) if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { - return model.NewAppError("BulkImport", "app.import.import_direct_post.create_group_channel.error", nil, "", http.StatusBadRequest) + return model.NewAppError("BulkImport", "app.import.import_direct_post.create_group_channel.error", nil, err.Error(), http.StatusBadRequest) } else { channel = ch } @@ -1548,7 +1548,7 @@ func (a *App) ImportDirectPost(data *DirectPostImportData, dryRun bool) *model.A if len(preferences) > 0 { if result := <-a.Srv.Store.Preference().Save(&preferences); result.Err != nil { - return model.NewAppError("BulkImport", "app.import.import_direct_post.save_preferences.error", nil, "", http.StatusInternalServerError) + return model.NewAppError("BulkImport", "app.import.import_direct_post.save_preferences.error", nil, result.Err.Error(), http.StatusInternalServerError) } } } diff --git a/cmd/commands/import.go b/cmd/commands/import.go index 4058d175a..c572ec53a 100644 --- a/cmd/commands/import.go +++ b/cmd/commands/import.go @@ -130,6 +130,7 @@ func bulkImportCmdF(command *cobra.Command, args []string) error { if lineNumber != 0 { cmd.CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber)) } + return err } else { if apply { cmd.CommandPrettyPrintln("Finished Bulk Import.") -- cgit v1.2.3-1-g7c22