diff --git a/.rubocop.yml b/.rubocop.yml
index e3db44c2e..8d8e8a64b 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,3 +1,5 @@
+inherit_from: .rubocop_todo.yml
+
AllCops:
Exclude:
- 'bin/*'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
new file mode 100644
index 000000000..30efa87e8
--- /dev/null
+++ b/.rubocop_todo.yml
@@ -0,0 +1,43 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config`
+# on 2025-11-04 02:06:37 UTC using RuboCop version 1.81.6.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 52
+# This cop supports safe autocorrection (--autocorrect).
+Lint/RedundantCopDisableDirective:
+ Enabled: false
+
+# Offense count: 2
+# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
+Metrics/MethodLength:
+ Max: 14
+
+# Offense count: 1
+# This cop supports safe autocorrection (--autocorrect).
+# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
+# SupportedStyles: assign_to_condition, assign_inside_condition
+Style/ConditionalAssignment:
+ Exclude:
+ - 'app/controllers/better_together/joatu/hub_controller.rb'
+
+# Offense count: 29
+# This cop supports safe autocorrection (--autocorrect).
+Style/IfUnlessModifier:
+ Enabled: false
+
+# Offense count: 1
+# Configuration parameters: Max.
+Style/SafeNavigationChainLength:
+ Exclude:
+ - 'spec/features/devise/registration_spec.rb'
+
+# Offense count: 1
+# This cop supports safe autocorrection (--autocorrect).
+# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
+# URISchemes: http, https
+Layout/LineLength:
+ Max: 145
diff --git a/Gemfile.lock b/Gemfile.lock
index 195b61b11..59849fe48 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -29,6 +29,7 @@ PATH
devise
devise-i18n
devise-jwt
+ devise_zxcvbn
elasticsearch-model (~> 7)
elasticsearch-rails (~> 7)
font-awesome-sass (~> 6.5)
@@ -266,6 +267,9 @@ GEM
devise-jwt (0.12.1)
devise (~> 4.0)
warden-jwt_auth (~> 0.10)
+ devise_zxcvbn (6.0.0)
+ devise
+ zxcvbn (~> 0.1.7)
diff-lcs (1.6.2)
disposable (0.6.3)
declarative (>= 0.0.9, < 1.0.0)
@@ -832,6 +836,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.3)
+ zxcvbn (0.1.13)
PLATFORMS
aarch64-linux
diff --git a/app/controllers/better_together/users/registrations_controller.rb b/app/controllers/better_together/users/registrations_controller.rb
index 256c727b7..faf140b26 100644
--- a/app/controllers/better_together/users/registrations_controller.rb
+++ b/app/controllers/better_together/users/registrations_controller.rb
@@ -7,6 +7,7 @@ class RegistrationsController < ::Devise::RegistrationsController # rubocop:todo
include DeviseLocales
skip_before_action :check_platform_privacy
+ before_action :configure_permitted_parameters
before_action :set_required_agreements, only: %i[new create]
before_action :set_event_invitation_from_session, only: %i[new create]
before_action :configure_account_update_params, only: [:update]
@@ -63,18 +64,12 @@ def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
def new
super do |user|
- # Pre-fill email from platform invitation
- user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty?
-
- if @event_invitation
- # Pre-fill email from event invitation
- user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty?
- user.person = @event_invitation.invitee if @event_invitation.invitee.present?
- end
+ setup_user_from_invitations(user)
+ user.build_person unless user.person
end
end
- def create # rubocop:todo Metrics/MethodLength
+ def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
unless agreements_accepted?
handle_agreements_not_accepted
return
@@ -87,11 +82,25 @@ def create # rubocop:todo Metrics/MethodLength
return
end
+ # Use transaction for all user creation and associated records
ActiveRecord::Base.transaction do
- super do |user|
- handle_user_creation(user) if user.persisted?
+ # Call Devise's default create behavior
+ super
+
+ # Handle post-registration setup if user was created successfully
+ if resource.persisted? && resource.errors.empty?
+ handle_user_creation(resource)
+ elsif resource.persisted?
+ # User was created but has errors - rollback to maintain consistency
+ raise ActiveRecord::Rollback
end
end
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::InvalidForeignKey => e
+ # Clean up and show user-friendly error
+ Rails.logger.error "Registration failed: #{e.message}"
+ build_resource(sign_up_params) if resource.nil?
+ resource&.errors&.add(:base, 'Registration could not be completed. Please try again.')
+ respond_with resource
end
protected
@@ -105,6 +114,10 @@ def configure_account_update_params
keys: %i[email password password_confirmation current_password])
end
+ def configure_permitted_parameters
+ devise_parameter_sanitizer.permit(:sign_up, keys: [person_attributes: %i[identifier name description]])
+ end
+
def set_required_agreements
@privacy_policy_agreement = BetterTogether::Agreement.find_by(identifier: 'privacy_policy')
@terms_of_service_agreement = BetterTogether::Agreement.find_by(identifier: 'terms_of_service')
@@ -170,32 +183,116 @@ def handle_agreements_not_accepted
respond_with resource
end
+ def setup_user_from_invitations(user)
+ # Pre-fill email from platform invitation
+ user.email = @platform_invitation.invitee_email if @platform_invitation && user.email.empty?
+
+ return unless @event_invitation
+
+ # Pre-fill email from event invitation
+ user.email = @event_invitation.invitee_email if @event_invitation && user.email.empty?
+ user.person = @event_invitation.invitee if @event_invitation.invitee.present?
+ end
+
def handle_user_creation(user)
- setup_person_for_user(user)
- return unless user.save!
+ return unless event_invitation_person_updated?(user)
+ # Reload user to ensure all nested attributes and associations are properly loaded
user.reload
- setup_community_membership(user)
+ person = user.person
+
+ return unless person_persisted?(user, person)
+
+ setup_community_membership(user, person)
handle_platform_invitation(user)
handle_event_invitation(user)
- create_agreement_participants(user.person)
+ create_agreement_participants(person)
+ end
+
+ def event_invitation_person_updated?(user)
+ return true unless @event_invitation&.invitee.present?
+
+ return true if user.person.update(person_params)
+
+ Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}"
+ false
+ end
+
+ def person_persisted?(user, person)
+ return true if person&.persisted?
+
+ Rails.logger.error "Person not found or not persisted for user #{user.id}"
+ false
end
def setup_person_for_user(user)
- if @event_invitation && @event_invitation.invitee.present?
- user.person = @event_invitation.invitee
- user.person.update(person_params)
- else
- user.build_person(person_params)
- end
+ return update_existing_person_for_event(user) if @event_invitation&.invitee.present?
+
+ create_new_person_for_user(user)
+ end
+
+ def update_existing_person_for_event(user)
+ user.person = @event_invitation.invitee
+ return if user.person.update(person_params)
+
+ Rails.logger.error "Failed to update person for event invitation: #{user.person.errors.full_messages}"
+ user.errors.add(:person, 'Could not update person information')
end
- def setup_community_membership(user)
+ def create_new_person_for_user(user)
+ return handle_empty_person_params(user) if person_params.empty?
+
+ user.build_person(person_params)
+ return unless person_validated_and_saved?(user)
+
+ save_person_identification(user)
+ end
+
+ def handle_empty_person_params(user)
+ Rails.logger.error 'Person params are empty, cannot build person'
+ user.errors.add(:person, 'Person information is required')
+ end
+
+ def person_validated_and_saved?(user)
+ return save_person?(user) if user.person.valid?
+
+ Rails.logger.error "Person validation failed: #{user.person.errors.full_messages}"
+ user.errors.add(:person, 'Person information is invalid')
+ false
+ end
+
+ def save_person?(user)
+ return true if user.person.save
+
+ Rails.logger.error "Failed to save person: #{user.person.errors.full_messages}"
+ user.errors.add(:person, 'Could not save person information')
+ false
+ end
+
+ def save_person_identification(user)
+ person_identification = user.person_identification
+ return if person_identification&.save
+
+ Rails.logger.error "Failed to save person identification: #{person_identification&.errors&.full_messages}"
+ user.errors.add(:person, 'Could not link person to user')
+ end
+
+ def setup_community_membership(user, person_param = nil)
+ person = person_param || user.person
community_role = determine_community_role
- helpers.host_community.person_community_memberships.find_or_create_by!(
- member: user.person,
- role: community_role
- )
+
+ begin
+ helpers.host_community.person_community_memberships.find_or_create_by!(
+ member: person,
+ role: community_role
+ )
+ rescue ActiveRecord::InvalidForeignKey => e
+ Rails.logger.error "Foreign key violation creating community membership: #{e.message}"
+ raise e
+ rescue StandardError => e
+ Rails.logger.error "Unexpected error creating community membership: #{e.message}"
+ raise e
+ end
end
def handle_platform_invitation(user)
@@ -235,10 +332,18 @@ def after_update_path_for(_resource)
end
def person_params
+ return {} unless params[:user] && params[:user][:person_attributes]
+
params.require(:user).require(:person_attributes).permit(%i[identifier name description])
+ rescue ActionController::ParameterMissing => e
+ Rails.logger.error "Missing person parameters: #{e.message}"
+ {}
end
def agreements_accepted?
+ # Ensure required agreements are set
+ set_required_agreements if @privacy_policy_agreement.nil?
+
required = [params[:privacy_policy_agreement], params[:terms_of_service_agreement]]
# If a code of conduct agreement exists, require it as well
required << params[:code_of_conduct_agreement] if @code_of_conduct_agreement.present?
@@ -247,11 +352,21 @@ def agreements_accepted?
end
def create_agreement_participants(person)
+ unless person&.persisted?
+ Rails.logger.error 'Cannot create agreement participants - person not persisted'
+ return
+ end
+
identifiers = %w[privacy_policy terms_of_service]
identifiers << 'code_of_conduct' if BetterTogether::Agreement.exists?(identifier: 'code_of_conduct')
agreements = BetterTogether::Agreement.where(identifier: identifiers)
+
agreements.find_each do |agreement|
- BetterTogether::AgreementParticipant.create!(agreement: agreement, person: person, accepted_at: Time.current)
+ BetterTogether::AgreementParticipant.create!(
+ agreement: agreement,
+ person: person,
+ accepted_at: Time.current
+ )
end
end
end
diff --git a/app/models/better_together/identification.rb b/app/models/better_together/identification.rb
index 2280e7241..2c5ce138c 100644
--- a/app/models/better_together/identification.rb
+++ b/app/models/better_together/identification.rb
@@ -10,6 +10,8 @@ class Identification < ApplicationRecord
polymorphic: true,
autosave: true
+ accepts_nested_attributes_for :identity
+
validates :identity,
presence: true
validates :agent,
diff --git a/app/models/better_together/user.rb b/app/models/better_together/user.rb
index ba815960c..8f89f3562 100644
--- a/app/models/better_together/user.rb
+++ b/app/models/better_together/user.rb
@@ -8,6 +8,7 @@ class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
+ :zxcvbnable,
:recoverable, :rememberable, :validatable, :confirmable,
:jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
@@ -32,31 +33,40 @@ class User < ApplicationRecord
delegate :permitted_to?, to: :person, allow_nil: true
def build_person(attributes = {})
- build_person_identification(
+ identification = build_person_identification(
agent: self,
- identity: ::BetterTogether::Person.new(attributes)
- )&.identity
+ identity_type: 'BetterTogether::Person',
+ active: true
+ )
+ person = ::BetterTogether::Person.new(attributes)
+ identification.identity = person
+ person
end
- # Define person_attributes method to get attributes of associated Person
+ # Override person method to ensure it builds if needed
def person
- # Check if a Person object exists and return its attributes
- super.present? ? super : build_person
+ person_identification&.identity
end
- # def person= arg
- # build_person_identification(
- # agent: self,
- # identity: arg
- # )&.identity
- # end
+ # Custom person= method
+ def person=(person_obj)
+ if person_identification
+ person_identification.identity = person_obj
+ else
+ build_person_identification(
+ agent: self,
+ identity: person_obj,
+ identity_type: 'BetterTogether::Person',
+ active: true
+ )
+ end
+ end
- # Define person_attributes= method
+ # Define person_attributes= method for nested attributes
def person_attributes=(attributes)
- # Check if a Person object already exists
- if person.present?
+ if person_identification&.identity
# Update existing Person object
- person.update(attributes)
+ person_identification.identity.assign_attributes(attributes)
else
# Build new Person object if it doesn't exist
build_person(attributes)
@@ -66,5 +76,12 @@ def person_attributes=(attributes)
def to_s
email
end
+
+ # TODO: accessing person here was causing save issues in the registration process due the the autobuild
+ # def weak_words
+ # return [] unless person
+
+ # [person.name, person.slug, person.identifier]
+ # end
end
end
diff --git a/app/views/better_together/joatu/offers/_offer_list_item.html.erb b/app/views/better_together/joatu/offers/_offer_list_item.html.erb
index b1b5e3603..c96c5f70b 100644
--- a/app/views/better_together/joatu/offers/_offer_list_item.html.erb
+++ b/app/views/better_together/joatu/offers/_offer_list_item.html.erb
@@ -14,7 +14,7 @@
<% response_link = joatu_offer.response_links_as_response.find { |rl| rl.source&.creator == current_person } rescue nil %>
<% if response_link.present? %>
<% source_type_key = response_link.source_type.demodulize.underscore rescue 'item' %>
- <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize), default: "Response to your %{type}", type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %>
+ <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %>
<% end %>
<% end %>
diff --git a/app/views/better_together/joatu/requests/_request_list_item.html.erb b/app/views/better_together/joatu/requests/_request_list_item.html.erb
index 661e1f470..e12f482e8 100644
--- a/app/views/better_together/joatu/requests/_request_list_item.html.erb
+++ b/app/views/better_together/joatu/requests/_request_list_item.html.erb
@@ -14,7 +14,7 @@
<% response_link = request_model.response_links_as_response.find { |rl| rl.source&.creator == current_person } rescue nil %>
<% if response_link.present? %>
<% source_type_key = response_link.source_type.demodulize.underscore rescue 'item' %>
- <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize), default: "Response to your %{type}", type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %>
+ <%= t('better_together.joatu.response_to_your', type: t("better_together.joatu.type.#{source_type_key}", default: response_link.source_type.demodulize)) %>
<% end %>
<% end %>
diff --git a/better_together.gemspec b/better_together.gemspec
index f7b86ab58..181229f90 100644
--- a/better_together.gemspec
+++ b/better_together.gemspec
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'devise'
spec.add_dependency 'devise-i18n'
spec.add_dependency 'devise-jwt'
+ spec.add_dependency 'devise_zxcvbn'
spec.add_dependency 'elasticsearch-model', '~> 7'
spec.add_dependency 'elasticsearch-rails', '~> 7'
spec.add_dependency 'font-awesome-sass', '~> 6.5'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6a49a1955..a8580eed1 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -409,6 +409,7 @@ en:
last_sign_in_ip: Last sign in IP
lock_version: Lock version
locked_at: Locked at
+ password: Password
person: :activerecord.models.person
person_identification: :activerecord.models.person_identification
remember_created_at: Remember created at
@@ -1847,6 +1848,9 @@ en:
too_short:
one: is too short (minimum is %{count} character)
other: is too short (minimum is %{count} characters)
+ weak_password: not strong enough. Consider adding a number, symbols or more
+ letters to make it stronger. Avoid using the word 'password', your birthday,
+ or other common easily-guessable passwords.
wrong_length:
one: is the wrong length (should be %{count} character)
other: is the wrong length (should be %{count} characters)
@@ -2041,6 +2045,7 @@ en:
en: English
es: Español
fr: Français
+ uk: Українська
meta:
default_description: Welcome to %{platform_name}
page:
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 31edccb05..8d35eafcf 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -409,6 +409,7 @@ es:
last_sign_in_ip: IP del último inicio
lock_version: Versión de bloqueo
locked_at: Fecha de bloqueo
+ password: Contraseña
person: :activerecord.models.person
person_identification: :activerecord.models.person_identification
remember_created_at: Fecha de 'Recordarme'
@@ -1869,6 +1870,9 @@ es:
too_short:
one: es demasiado corto (%{count} carácter mínimo)
other: es demasiado corto (%{count} caracteres mínimo)
+ weak_password: no es lo suficientemente fuerte. Considera agregar un número,
+ símbolos o más letras para hacerla más fuerte. Evita usar la palabra 'contraseña',
+ tu fecha de nacimiento o otras contraseñas comunes fáciles de adivinar.
wrong_length:
one: no tiene la longitud correcta (%{count} carácter exactos)
other: no tiene la longitud correcta (%{count} caracteres exactos)
@@ -2067,6 +2071,7 @@ es:
en: English
es: Español
fr: Français
+ uk: Українська
meta:
default_description: Bienvenido a %{platform_name}
page:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 60a48b25e..b154fe803 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -409,6 +409,7 @@ fr:
last_sign_in_ip: IP de dernière connexion
lock_version: Version de verrouillage
locked_at: Verrouillé à
+ password: Mot de passe
person: :activerecord.models.person
person_identification: :activerecord.models.person_identification
remember_created_at: Souvenir créé à
@@ -1877,6 +1878,9 @@ fr:
too_short:
one: est trop court (au moins %{count} caractère)
other: est trop court (au moins %{count} caractères)
+ weak_password: n'est pas assez fort. Considérez ajouter un nombre, des symboles
+ ou plus de lettres pour le renforcer. Évitez d'utiliser le mot 'mot de passe',
+ votre date de naissance ou d'autres mots de passe communs faciles à deviner.
wrong_length:
one: ne fait pas la bonne longueur (doit comporter %{count} caractère)
other: ne fait pas la bonne longueur (doit comporter %{count} caractères)
@@ -2072,8 +2076,9 @@ fr:
calculating: Calcul en cours
locales:
en: English
- es: Espagnol
+ es: Español
fr: Français
+ uk: Українська
meta:
default_description: Bienvenue sur %{platform_name}
page:
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
new file mode 100644
index 000000000..44f3b2d3c
--- /dev/null
+++ b/config/locales/uk.yml
@@ -0,0 +1,2326 @@
+---
+uk:
+ accepted_at: Прийнято о
+ accessibility_attributes: Атрибути доступності
+ activerecord:
+ attributes:
+ action_text/encrypted_rich_text:
+ body: Тіло
+ embeds_attachments: Вбудовані вкладення
+ embeds_blobs: Вбудовані блоби
+ locale: Локаль
+ name: Ім'я
+ record: :activerecord.models.record
+ record_type: Тип запису
+ action_text/rich_text:
+ body: Тіло
+ embeds_attachments: Вбудовані вкладення
+ embeds_blobs: Вбудовані блоби
+ locale: Локаль
+ name: Ім'я
+ record: :activerecord.models.record
+ record_type: Тип запису
+ active_storage/attachment:
+ blob: :activerecord.models.blob
+ name: Ім'я
+ record: :activerecord.models.record
+ record_type: Тип запису
+ active_storage/blob:
+ attachments: Вкладення
+ byte_size: Розмір у байтах
+ checksum: Контрольна сума
+ content_type: Тип контенту
+ filename: Ім'я файлу
+ key: Ключ
+ metadata: Метадані
+ preview_image_attachment: :activerecord.models.preview_image_attachment
+ preview_image_blob: :activerecord.models.preview_image_blob
+ service_name: Ім'я сервісу
+ variant_records: Записи варіантів
+ active_storage/variant_record:
+ blob: :activerecord.models.blob
+ image_attachment: :activerecord.models.image_attachment
+ image_blob: :activerecord.models.image_blob
+ variation_digest: Дайджест варіації
+ application_notifier:
+ notifications: Сповіщення
+ params: Параметри
+ record: :activerecord.models.record
+ record_type: Тип запису
+ application_notifier/notification:
+ event: :activerecord.models.event
+ read_at: Прочитано о
+ recipient: :activerecord.models.recipient
+ recipient_type: Тип одержувача
+ seen_at: Переглянуто о
+ better_together/community:
+ creator: :activerecord.models.creator
+ host: Хост
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ person_community_memberships: Членство осіб у спільноті
+ person_members: Учасники особи
+ person_roles: Ролі особи
+ privacy: Конфіденційність
+ protected: Захищено
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ better_together/content/block:
+ accessibility_attributes: Атрибути доступності
+ content_settings: Налаштування контенту
+ css_settings: Налаштування CSS
+ data_attributes: Атрибути даних
+ html_attributes: HTML атрибути
+ identifier: Ідентифікатор
+ layout_settings: Налаштування макета
+ lock_version: Версія блокування
+ media_settings: Налаштування медіа
+ page_blocks: Блоки сторінки
+ pages: Сторінки
+ better_together/content/image:
+ accessibility_attributes: Атрибути доступності
+ content_settings: Налаштування контенту
+ css_settings: Налаштування CSS
+ data_attributes: Атрибути даних
+ html_attributes: HTML атрибути
+ identifier: Ідентифікатор
+ layout_settings: Налаштування макета
+ lock_version: Версія блокування
+ media_attachment: :activerecord.models.media_attachment
+ media_blob: :activerecord.models.media_blob
+ media_settings: Налаштування медіа
+ page_blocks: Блоки сторінки
+ pages: Сторінки
+ string_translations: Переклади рядків
+ better_together/content/page_block:
+ block: :activerecord.models.block
+ lock_version: Версія блокування
+ page: :activerecord.models.page
+ position: Позиція
+ better_together/content/rich_text:
+ accessibility_attributes: Атрибути доступності
+ content_settings: Налаштування контенту
+ css_settings: Налаштування CSS
+ data_attributes: Атрибути даних
+ html_attributes: HTML атрибути
+ identifier: Ідентифікатор
+ layout_settings: Налаштування макета
+ lock_version: Версія блокування
+ media_settings: Налаштування медіа
+ page_blocks: Блоки сторінки
+ pages: Сторінки
+ rich_text_content: :activerecord.models.rich_text_content
+ rich_text_content_en: :activerecord.models.rich_text_content_en
+ rich_text_content_es: :activerecord.models.rich_text_content_es
+ rich_text_content_fr: :activerecord.models.rich_text_content_fr
+ rich_text_translations: Переклади багатого тексту
+ better_together/conversation:
+ conversation_participants: Учасники розмови
+ creator: :activerecord.models.creator
+ lock_version: Версія блокування
+ messages: Повідомлення
+ participants: Учасники
+ title: Заголовок розмови
+ better_together/conversation_participant:
+ conversation: :activerecord.models.conversation
+ lock_version: Версія блокування
+ person: :activerecord.models.person
+ better_together/geography/continent:
+ community: :activerecord.models.community
+ countries: Країни
+ country_continents: Континенти країн
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ protected: Захищено
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ better_together/geography/country:
+ community: :activerecord.models.community
+ continents: Континенти
+ country_continents: Континенти країн
+ identifier: Ідентифікатор
+ iso_code: ISO код
+ lock_version: Версія блокування
+ protected: Захищено
+ slug: Слаг
+ slugs: Слаги
+ states: Штати
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ better_together/geography/country_continent:
+ continent: :activerecord.models.continent
+ country: :activerecord.models.country
+ lock_version: Версія блокування
+ better_together/geography/region:
+ community: :activerecord.models.community
+ country: :activerecord.models.country
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ protected: Захищено
+ region_settlements: Поселення регіону
+ settlements: Поселення
+ slug: Слаг
+ slugs: Слаги
+ state: :activerecord.models.state
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ better_together/geography/region_settlement:
+ lock_version: Версія блокування
+ protected: Захищено
+ region: :activerecord.models.region
+ settlement: :activerecord.models.settlement
+ better_together/geography/settlement:
+ community: :activerecord.models.community
+ country: :activerecord.models.country
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ protected: Захищено
+ region_settlements: Поселення регіону
+ regions: Регіони
+ slug: Слаг
+ slugs: Слаги
+ state: :activerecord.models.state
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ better_together/geography/state:
+ community: :activerecord.models.community
+ country: :activerecord.models.country
+ identifier: Ідентифікатор
+ iso_code: ISO код
+ lock_version: Версія блокування
+ protected: Захищено
+ regions: Регіони
+ settlements: Поселення
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ better_together/identification:
+ active: Активний
+ agent: :activerecord.models.agent
+ agent_type: Тип агента
+ identity: :activerecord.models.identity
+ identity_type: Тип ідентичності
+ lock_version: Версія блокування
+ better_together/jwt_denylist:
+ exp: Закінчення
+ jti: JWT ідентифікатор
+ lock_version: Версія блокування
+ better_together/message:
+ content: Зміст
+ conversation: :activerecord.models.conversation
+ lock_version: Версія блокування
+ sender: :activerecord.models.sender
+ better_together/navigation_area:
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ name: Назва
+ navigable: :activerecord.models.navigable
+ navigable_type: Тип навігації
+ navigation_items: Елементи навігації
+ protected: Захищено
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ style: Стиль
+ visible: Видимий
+ better_together/navigation_item:
+ children: Дочірні елементи
+ icon: Іконка
+ identifier: Ідентифікатор
+ item_type: Тип елемента
+ linkable: :activerecord.models.linkable
+ linkable_type: Тип посилання
+ lock_version: Версія блокування
+ navigation_area: :activerecord.models.navigation_area
+ parent: :activerecord.models.parent
+ position: Позиція
+ protected: Захищено
+ route_name: Назва маршруту
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ url: URL-адреса
+ visible: Видимий
+ better_together/new_message_notifier:
+ notifications: Сповіщення
+ params: Параметри
+ record: :activerecord.models.record
+ record_type: Тип запису
+ better_together/new_message_notifier/notification:
+ event: :activerecord.models.event
+ read_at: Прочитано о
+ recipient: :activerecord.models.recipient
+ recipient_type: Тип одержувача
+ seen_at: Переглянуто о
+ better_together/page:
+ blocks: Блоки
+ identifier: Ідентифікатор
+ image_blocks: Блоки зображень
+ keywords: Ключові слова
+ language: Мова
+ layout: Макет
+ lock_version: Версія блокування
+ meta_description: Мета-опис
+ page_blocks: Блоки сторінки
+ privacy: Конфіденційність
+ protected: Захищено
+ published: Опубліковано
+ published_at: Опубліковано о
+ rich_text_blocks: Блоки багатого тексту
+ rich_text_content: :activerecord.models.rich_text_content
+ rich_text_content_en: :activerecord.models.rich_text_content_en
+ rich_text_content_es: :activerecord.models.rich_text_content_es
+ rich_text_content_fr: :activerecord.models.rich_text_content_fr
+ rich_text_translations: Переклади багатого тексту
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ template: Шаблон
+ better_together/page_authorship_notifier:
+ notifications: Сповіщення
+ params: Параметри
+ record: :activerecord.models.record
+ record_type: Тип запису
+ better_together/page_authorship_notifier/notification:
+ event: :activerecord.models.event
+ read_at: Прочитано о
+ recipient: :activerecord.models.recipient
+ recipient_type: Тип одержувача
+ seen_at: Переглянуто о
+ better_together/person:
+ authorships: Авторство
+ community: :activerecord.models.community
+ community_roles: Ролі в спільноті
+ conversation_participants: Учасники розмови
+ conversations: Розмови
+ created_conversations: Створені розмови
+ description: Опис
+ identifications: Ідентифікації
+ identifier: Ідентифікатор
+ locale: Мова
+ lock_version: Версія блокування
+ member_communities: Членські спільноти
+ member_platforms: Членські платформи
+ name: Ім'я
+ notification_mentions: Згадки в сповіщеннях
+ notifications: Сповіщення
+ notify_by_email: Отримувати сповіщення електронною поштою
+ person_community_memberships: Членство особи в спільноті
+ person_platform_memberships: Членство особи в платформі
+ platform_roles: Ролі на платформі
+ preferences: Налаштування
+ show_conversation_details: Показати деталі розмови
+ slug: Ім'я користувача
+ slugs: Слаги
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ user: :activerecord.models.user
+ user_identification: :activerecord.models.user_identification
+ better_together/person_community_membership:
+ joinable: :activerecord.models.joinable
+ lock_version: Версія блокування
+ member: :activerecord.models.member
+ role: :activerecord.models.role
+ better_together/person_platform_membership:
+ joinable: :activerecord.models.joinable
+ lock_version: Версія блокування
+ member: :activerecord.models.member
+ role: :activerecord.models.role
+ better_together/platform:
+ community: :activerecord.models.community
+ host: Хост
+ identifier: Ідентифікатор
+ invitations: Запрошення
+ lock_version: Версія блокування
+ person_members: Учасники
+ person_platform_memberships: Членство осіб на платформі
+ person_roles: Ролі осіб
+ privacy: Конфіденційність
+ protected: Захищено
+ settings: Налаштування
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ time_zone: Часовий пояс
+ url: URL-адреса
+ better_together/platform_invitation:
+ accepted_at: Прийнято
+ community_role: :activerecord.models.community_role
+ created_at: Створено
+ invitable: :activerecord.models.invitable
+ invitee: :activerecord.models.invitee
+ invitee_email: Електронна пошта запрошеного
+ inviter: :activerecord.models.inviter
+ last_sent: Останнє відправлення
+ locale: Мова
+ lock_version: Версія блокування
+ platform_role: :activerecord.models.platform_role
+ session_duration_mins: Тривалість сесії (хв)
+ status: Статус
+ token: Токен
+ type: Тип
+ valid_from: Дійсний з
+ valid_until: Дійсний до
+ better_together/resource_permission:
+ action: Дія
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ position: Позиція
+ protected: Захищено
+ resource_type: Тип ресурсу
+ role_resource_permissions: Дозволи ролей для ресурсів
+ roles: Ролі
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ target: Ціль
+ better_together/role:
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ position: Позиція
+ protected: Захищено
+ resource_permissions: Дозволи ресурсів
+ resource_type: Тип ресурсу
+ role_resource_permissions: Дозволи ролей для ресурсів
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ text_translations: Переклади тексту
+ better_together/role_resource_permission:
+ lock_version: Версія блокування
+ resource_permission: :activerecord.models.resource_permission
+ role: :activerecord.models.role
+ better_together/user:
+ confirmation_sent_at: Підтвердження надіслано о
+ confirmation_token: Токен підтвердження
+ confirmed_at: Підтверджено о
+ current_sign_in_at: Поточний вхід о
+ current_sign_in_ip: IP поточного входу
+ email: Електронна пошта
+ encrypted_password: Зашифрований пароль
+ failed_attempts: Невдалі спроби
+ last_sign_in_at: Останній вхід о
+ last_sign_in_ip: IP останнього входу
+ lock_version: Версія блокування
+ locked_at: Заблоковано о
+ password: Пароль
+ person: :activerecord.models.person
+ person_identification: :activerecord.models.person_identification
+ remember_created_at: Запам'ятати створено о
+ reset_password_sent_at: Скидання пароля надіслано о
+ reset_password_token: Токен скидання пароля
+ slugs: Слаги
+ string_translations: Переклади рядків
+ unconfirmed_email: Непідтверджена електронна пошта
+ unlock_token: Токен розблокування
+ better_together/wizard:
+ current_completions: Поточні завершення
+ first_completed_at: Вперше завершено о
+ identifier: Ідентифікатор
+ last_completed_at: Останнє завершено о
+ lock_version: Версія блокування
+ max_completions: Максимальні завершення
+ protected: Захищено
+ slug: Слаг
+ slugs: Слаги
+ string_translations: Переклади рядків
+ success_message: Повідомлення про успіх
+ success_path: Шлях успіху
+ text_translations: Переклади тексту
+ wizard_step_definitions: Визначення кроків майстра
+ wizard_steps: Кроки майстра
+ better_together/wizard_step:
+ completed: Завершено
+ creator: :activerecord.models.creator
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ step_number: Номер кроку
+ wizard: :activerecord.models.wizard
+ wizard_step_definition: :activerecord.models.wizard_step_definition
+ better_together/wizard_step_definition:
+ form_class: Клас форми
+ identifier: Ідентифікатор
+ lock_version: Версія блокування
+ message: Повідомлення
+ protected: Захищено
+ slug: Слаг
+ slugs: Слаги
+ step_number: Номер кроку
+ string_translations: Переклади рядків
+ template: Шаблон
+ text_translations: Переклади тексту
+ wizard: :activerecord.models.wizard
+ wizard_steps: Кроки майстра
+ friendly_id/slug:
+ locale: Locale
+ lock_version: Lock version
+ scope: Scope
+ slug: Slug
+ sluggable: :activerecord.models.sluggable
+ sluggable_type: Sluggable type
+ mobility/backends/action_text/plain_text_translation:
+ body: Body
+ locale: Locale
+ name: Name
+ record: :activerecord.models.record
+ record_type: Record type
+ mobility/backends/action_text/rich_text_translation:
+ body: Body
+ embeds_attachments: Embeds attachments
+ embeds_blobs: Embeds blobs
+ locale: Locale
+ name: Name
+ record: :activerecord.models.record
+ record_type: Record type
+ mobility/backends/active_record/key_value/string_translation:
+ key: Key
+ locale: Locale
+ translatable: :activerecord.models.translatable
+ translatable_type: Translatable type
+ value: Value
+ mobility/backends/active_record/key_value/text_translation:
+ key: Key
+ locale: Locale
+ translatable: :activerecord.models.translatable
+ translatable_type: Translatable type
+ value: Value
+ noticed/event:
+ notifications: Notifications
+ params: Params
+ record: :activerecord.models.record
+ record_type: Record type
+ noticed/notification:
+ event: :activerecord.models.event
+ read_at: Read at
+ recipient: :activerecord.models.recipient
+ recipient_type: Recipient type
+ seen_at: Seen at
+ errors:
+ messages:
+ record_invalid: 'Validation failed: %{errors}'
+ restrict_dependent_destroy:
+ has_many: Cannot delete record because dependent %{record} exist
+ has_one: Cannot delete record because a dependent %{record} exists
+ models:
+ action_text/encrypted_rich_text: зашифрований багатий текст
+ action_text/rich_text: багатий текст
+ active_storage/attachment: вкладення
+ active_storage/blob: блоб
+ active_storage/variant_record: запис варіанта
+ application_notifier: Сповіщувач програми
+ application_notifier/notification: Сповіщення
+ better_together/community: Спільнота
+ better_together/content/block: Блок
+ better_together/content/css: Користувацький CSS
+ better_together/content/hero: Герой
+ better_together/content/html: HTML
+ better_together/content/image: Зображення
+ better_together/content/page_block: Блок сторінки
+ better_together/content/rich_text: Багатий текст
+ better_together/content/template: Файл шаблону
+ better_together/conversation: Розмова
+ better_together/conversation_participant: Учасник розмови
+ better_together/geography/continent: Континент
+ better_together/geography/country: Країна
+ better_together/geography/country_continent: Континент країни
+ better_together/geography/region: Регіон
+ better_together/geography/region_settlement: Поселення регіону
+ better_together/geography/settlement: Поселення
+ better_together/geography/state: Штат
+ better_together/guest_access: Гостьовий доступ
+ better_together/identification: Ідентифікація
+ better_together/infrastructure/building: Будівля
+ better_together/joatu/category: Категорія пропозицій та запитів
+ better_together/jwt_denylist: JWT чорний список
+ better_together/message: Повідомлення
+ better_together/navigation_area: Область навігації
+ better_together/navigation_item: Елемент навігації
+ better_together/new_message_notifier: Сповіщувач нових повідомлень
+ better_together/new_message_notifier/notification: сповіщувач нових повідомлень/сповіщення
+ better_together/page: Сторінка
+ better_together/person: Особа
+ better_together/person_community_membership: Членство особи в спільноті
+ better_together/person_platform_membership: Членство особи в платформі
+ better_together/platform: Платформа
+ better_together/platform_invitation: Запрошення платформи
+ better_together/resource_permission: Дозвіл ресурсу
+ better_together/role: Роль
+ better_together/role_resource_permission: Дозвіл ролі ресурсу
+ better_together/user: Користувач
+ better_together/wizard: Майстер
+ better_together/wizard_step: Крок майстра
+ better_together/wizard_step_definition: Визначення кроку майстра
+ friendly_id/slug: Слаг
+ mobility/backends/action_text/plain_text_translation: переклад звичайного тексту
+ mobility/backends/action_text/rich_text_translation: переклад багатого тексту
+ mobility/backends/active_record/key_value/string_translation: переклад рядка
+ mobility/backends/active_record/key_value/text_translation: переклад тексту
+ noticed/event: подія
+ noticed/notification: сповіщення
+ attributes:
+ privacy_list:
+ community: Спільнота
+ private: Приватний
+ public: Публічний
+ better_together:
+ addresses:
+ add: Додати адресу
+ address: Адреса
+ city_name: Місто
+ country_name: Країна
+ label: Мітка
+ labels:
+ billing: Рахунок
+ home: Дім
+ mailing: Поштова
+ main: Основна
+ other: Інша
+ physical: Фізична
+ shipping: Доставка
+ work: Робота
+ line1: Адреса рядок 1
+ line2: Адреса рядок 2
+ physical: Фізична
+ postal: Поштова
+ postal_code: Поштовий індекс
+ state_province_name: Штат/Провінція
+ title: Адреси
+ agreement_terms:
+ add: Додати
+ title: Умови угоди
+ agreements:
+ form:
+ page_help: Якщо встановлено, буде відображено вміст пов'язаної сторінки замість
+ умов угоди.
+ page_label: Пов'язана сторінка (необов'язково)
+ no_terms: Умови не визначено.
+ notice:
+ body: Будь ласка, перегляньте повну угоду перед прийняттям.
+ close: Закрити
+ title: Переглянути обов'язкову угоду
+ participant:
+ pending: очікує
+ show:
+ title: Угода
+ terms: Умови
+ view: Переглянути
+ authorship_mailer:
+ authorship_changed_notification:
+ greeting: Привіт %{recipient_name},
+ intro_added: Вас додано як автора до сторінки %{page}.
+ intro_added_by: "%{actor_name} додав вас як автора до сторінки %{page}."
+ intro_removed: Вас видалено як автора зі сторінки %{page}.
+ intro_removed_by: "%{actor_name} видалив вас як автора зі сторінки %{page}."
+ signature_html: "— Команда %{platform}"
+ subject_added: Вас додано як автора до %{page}
+ subject_added_by: "%{actor_name} додав вас як автора до %{page}"
+ subject_removed: Ваше авторство видалено з %{page}
+ subject_removed_by: "%{actor_name} видалив ваше авторство з %{page}"
+ view_page: Переглянути сторінку
+ view_page_link: Натисніть тут, щоб переглянути сторінку
+ buildings:
+ add: Додати
+ title: Будівлі
+ calendars:
+ default_description: Календар за замовчуванням для %s
+ personal_calendar_name: Особистий календар %{name}
+ calls_for_interest:
+ back_to_calls_for_interest: Повернутися до закликів до зацікавленості
+ hints:
+ description: Опис
+ ends_at: Закінчується о
+ name: Ім'я
+ slug: Слаг
+ starts_at: Починається о
+ labels:
+ ends_at: Закінчується о
+ privacy: Конфіденційність
+ starts_at: Починається о
+ none-yet: Поки що немає
+ save_call_for_interest: Зберегти заклик до зацікавленості
+ tabs:
+ details: Деталі
+ images: Зображення
+ time-and-place: Час і місце
+ view_call_for_interest: Переглянути заклик до зацікавленості
+ categories:
+ back_to_categories: Назад до категорій
+ index:
+ new_btn_text: Нова категорія
+ save_category: Зберегти категорію
+ view_category: Переглянути категорію
+ checklist_item:
+ update_failed: Помилка при оновленні елемента списку.
+ updated: Елемент списку оновлено.
+ checklist_items:
+ created: Елемент створено
+ description: Опис
+ edit_title: Редагувати елемент чек-листа
+ form_hint: Надайте коротку мітку та необов'язкові деталі. Використовуйте конфіденційність
+ для контролю хто бачить цей елемент.
+ hint_description: Необов'язкові деталі або інструкції для цього елемента. Підтримує
+ форматування багатого тексту.
+ hint_label: Короткий заголовок для елемента чек-листа (обов'язково).
+ hint_parent: 'Необов''язково: вкласти цей елемент під інший. Максимальна глибина
+ вкладення становить 2.'
+ hint_privacy: 'Виберіть, хто може бачити цей елемент: Публічний або Приватний.'
+ label: Мітка
+ move_down: Перемістити вниз
+ move_up: Перемістити вгору
+ new_title: Новий елемент чек-листа
+ parent: Батьківський елемент (необов'язково)
+ reorder: Переупорядкувати елемент
+ sign_in_to_toggle: Увійдіть, щоб відмітити цей елемент як виконаний
+ untitled: Елемент без назви
+ checklists:
+ edit:
+ title: Редагувати чек-лист
+ index:
+ new: Новий чек-лист
+ title: Чек-листи
+ new:
+ title: Новий чек-лист
+ show:
+ completed_message: Чек-лист завершено
+ items_title: Елементи
+ no_items: Немає елементів для відображення
+ communities:
+ index:
+ new_btn_text: Нова спільнота
+ new:
+ title: Спільноти
+ none-yet: Поки що немає
+ show:
+ create_event: Створити подію
+ tabs:
+ buildings: Будівлі
+ contact_details: Контактні деталі
+ details: Деталі
+ images: Зображення
+ contact_details:
+ contact_information: Контактна інформація
+ title: Контактні деталі
+ contactable:
+ addresses: Адреси
+ contact: Контакт
+ contacts: Контакти
+ email_addresses: Адреси електронної пошти
+ phone_numbers: Номери телефонів
+ social_media_accounts: Акаунти соціальних мереж
+ website_links: Посилання на веб-сайти
+ contacts:
+ add: Add Contact
+ content:
+ blocks:
+ associated_pages: Associated Pages
+ conversation_mailer:
+ new_message_notification:
+ from_address: Нове повідомлення через %{from_address}
+ from_address_with_sender: "%{sender_name} через %{from_address}"
+ greeting: Привіт %{recipient_name},
+ message_intro: У вас є непрочитане повідомлення
+ message_intro_with_sender: У вас є непрочитане повідомлення від %{sender_name}
+ signature: Підпис
+ signature_html: |-
+ З найкращими побажаннями,
+ Команда %{platform}
+ subject: У вашій розмові є непрочитане повідомлення
+ subject_with_title: "[%{conversation}] розмова має непрочитане повідомлення"
+ view_conversation: 'Ви можете переглянути та відповісти на це повідомлення,
+ натиснувши посилання нижче:'
+ view_conversation_link: Перейти до розмови
+ conversations:
+ communicator:
+ active_conversations: Активні розмови
+ add_participants: Додати учасників
+ conversations: Розмови
+ create_conversation: Створити розмову
+ last_message: Останнє повідомлення
+ new: Нова
+ new_conversation: Нова розмова
+ conversation:
+ last_message: Останнє повідомлення
+ left: Ви покинули розмову %{conversation}
+ conversation_content:
+ edit_conversation: Редагувати розмову
+ leave_conversation: Покинути розмову
+ options_tooltip: Параметри розмови
+ empty:
+ no_messages: Повідомлень поки що немає. Чому б не почати розмову?
+ errors:
+ no_permitted_participants: Ви можете додавати лише менеджерів платформи або
+ учасників, які дали згоду на отримання повідомлень.
+ form:
+ add_participants: Додати учасників
+ create_conversation: Створити розмову
+ first_message: Перше повідомлення
+ first_message_hint: Напишіть відкриваюче повідомлення для розмови
+ participants_hint: Виберіть одну або кількох людей для включення до розмови
+ title_hint: Необов'язково. Короткий описовий заголовок може допомогти учасникам
+ знайти цю розмову пізніше
+ index:
+ conversations: Розмови
+ new: Нова
+ layout_with_sidebar:
+ active_conversations: Активні розмови
+ add_participants: Додати учасників
+ conversations: Розмови
+ create_conversation: Створити розмову
+ last_message: Останнє повідомлення
+ new: Нова
+ new_conversation: Нова розмова
+ list:
+ add_participants: Додати учасників
+ conversations: Розмови
+ create_conversation: Створити розмову
+ new: Нова
+ new_conversation: Нова розмова
+ new:
+ add_participants: Додати учасників
+ create_conversation: Створити розмову
+ new_conversation: Нова розмова
+ sidebar:
+ conversations: Розмови
+ new: Нова
+ device_permissions:
+ location:
+ denied: Відхилено
+ enabled: Увімкнено
+ unsupported: Не підтримується
+ status:
+ denied: Відхилено
+ granted: Надано
+ unknown: Невідомо
+ email_addresses:
+ add: Додати адресу електронної пошти
+ email: Електронна пошта
+ label: Мітка
+ labels:
+ other: Інша
+ personal: Особиста
+ school: Школа
+ work: Робота
+ title: Адреси електронної пошти
+ event_invitations_mailer:
+ invite:
+ event_details: Деталі події
+ greeting: Привіт,
+ invited_by_html: Вас запросив %{inviter_name}.
+ invited_html: Вас запрошено на подію %{event_name}.
+ need_account_html: Вам потрібно створити обліковий запис, щоб прийняти це
+ запрошення та приєднатися до події.
+ review_invitation: Переглянути запрошення
+ subject: Вас запрошують на подію
+ when: Коли
+ where: Де
+ event_mailer:
+ event_reminder:
+ description: Опис
+ duration: Тривалість
+ ends_at: Закінчується о
+ greeting: Привіт %{recipient_name},
+ hours:
+ few: "%{count} години"
+ many: "%{count} годин"
+ one: "%{count} година"
+ other: "%{count} годин"
+ location: Місце
+ register_link: Зареєструватися або RSVP
+ reminder_message: Це дружнє нагадування про майбутню подію "%{event_name}".
+ signature_html: |-
+ З найкращими побажаннями,
+ Команда %{platform}
+ subject: 'Нагадування: %{event_name}'
+ time_tbd: Час буде оголошено
+ view_event_link_html: 'Переглянути деталі події: %{link}'
+ when: Коли
+ event_update:
+ changes_made: Changes made
+ current_details: Current details
+ description: Description
+ duration: Duration
+ ends_at: Ends at
+ greeting: Привіт %{recipient_name},
+ hours:
+ few: "%{count} години"
+ many: "%{count} годин"
+ one: "%{count} година"
+ other: "%{count} годин"
+ location: Місце
+ register_link: Register or RSVP
+ signature_html: |-
+ Best regards,
+ The %{platform} Team
+ subject: 'Event updated: %{event_name}'
+ time_tbd: Time tbd
+ update_message_plural: The event "%{event_name}" has been updated with several
+ changes.
+ update_message_singular: The event "%{event_name}" has been updated.
+ view_event_link_html: 'View event details: %{link}'
+ when: When
+ events:
+ actions:
+ create_new_address: Create new address
+ create_new_building: Create new building
+ create_new_short: New
+ add_to_calendar: Add to calendar (.ics)
+ attendees: Attendees
+ back_to_events: View Events
+ greater_than_or_equal_to: must be greater than or equal to %{count}
+ hints:
+ create_permission_denied: Дозвіл на створення відхилено
+ description: Опис
+ duration_minutes: Скільки часу триватиме подія. Змінюється автоматично при
+ зміні часу початку або закінчення.
+ ends_at: Закінчується о
+ location: Місце
+ location_name: Назва місця
+ name: Назва
+ select_address: Вибрати адресу
+ select_building: Вибрати будівлю
+ slug: Слаг
+ starts_at: Починається о
+ hosted_by: Організатор
+ ics:
+ view_details_url: URL деталей перегляду
+ in: має бути в %{count}
+ inclusion: не включено до списку
+ invalid: недійсний
+ labels:
+ duration_minutes: Тривалість
+ ends_at: Закінчується о
+ location: Місце
+ location_name: Назва місця
+ location_type: Тип місця
+ privacy: Конфіденційність
+ select_address: Вибрати адресу
+ select_building: Вибрати будівлю
+ starts_at: Починається о
+ less_than: має бути менше %{count}
+ less_than_or_equal_to: має бути менше або дорівнювати %{count}
+ location_types:
+ address: Адреса
+ building: Будівля
+ simple: Простий
+ login_required: Будь ласка, увійдіть для управління RSVP.
+ model_invalid: 'Помилка валідації: %{errors}'
+ no_attendees: Учасників поки що немає.
+ no_description_available: Опис недоступний.
+ none-yet: Поки що немає
+ not_a_number: не є числом
+ not_an_integer: має бути цілим числом
+ not_found: не знайдено
+ not_locked: не було заблоковано
+ not_saved:
+ few: 'Виникло %{count} помилки під час збереження %{resource}:'
+ many: 'Виникло %{count} помилок під час збереження %{resource}:'
+ one: 'Виникла помилка під час збереження %{resource}:'
+ other: 'Виникло %{count} помилок під час збереження %{resource}:'
+ odd: має бути непарним
+ other_than: має бути іншим ніж %{count}
+ placeholders:
+ location_name: Назва місця
+ present: має бути порожнім
+ prompts:
+ select_address: Вибрати адресу
+ select_building: Вибрати будівлю
+ register: Зареєструватися
+ relationship:
+ calendar: Подія календаря
+ created: Створено вами
+ going: Ви йдете
+ interested: Ви зацікавлені
+ required: має існувати
+ rsvp_cancel: Скасувати RSVP
+ rsvp_cancelled: RSVP скасовано
+ rsvp_counts: 'Йдуть: %{going} · Зацікавлені: %{interested}'
+ rsvp_going: Йду
+ rsvp_interested: Зацікавлений
+ rsvp_not_available: RSVP недоступне для цієї події.
+ rsvp_saved: RSVP збережено
+ rsvp_unavailable_draft: RSVP буде доступним після планування цієї події.
+ save_event: Зберегти подію
+ tabs:
+ details: Деталі
+ images: Зображення
+ time-and-place: Час і місце
+ taken: вже зайнято
+ time:
+ hour: година
+ hours: годин
+ minutes: хвилин
+ too_long:
+ few: занадто довгий (максимум %{count} символи)
+ many: занадто довгий (максимум %{count} символів)
+ one: занадто довгий (максимум %{count} символ)
+ other: занадто довгий (максимум %{count} символів)
+ too_short:
+ few: занадто короткий (мінімум %{count} символи)
+ many: занадто короткий (мінімум %{count} символів)
+ one: занадто короткий (мінімум %{count} символ)
+ other: занадто короткий (мінімум %{count} символів)
+ units:
+ minutes: minutes
+ view_event: View event
+ wrong_length:
+ few: неправильна довжина (точно %{count} символи)
+ many: неправильна довжина (точно %{count} символів)
+ one: неправильна довжина (точно %{count} символ)
+ other: неправильна довжина (точно %{count} символів)
+ geography:
+ locatable_location:
+ errors:
+ no_location_source: Must specify either a name or location
+ regions:
+ index:
+ title: Regions
+ settlements:
+ index:
+ title: Settlements
+ global:
+ add: Add
+ hub:
+ activities:
+ back_to_hub: Back to Hub
+ page_title: All Activity
+ index:
+ activity: Activity
+ announcements: Announcements
+ new_members: New members
+ page_title: Community Hub
+ platform_updates: Platform Updates
+ recommended_communities: Recommended Communities
+ trending_topics: Trending topics
+ upcoming_events: Upcoming Events
+ view_all_activity: View all
+ recent_offers_card:
+ new_offer: New Offer
+ no_offers: No recent offers
+ recent_offers: Recent Offers
+ view_all_offers: View all
+ recent_requests_card:
+ new_request: New Request
+ no_requests: No recent requests
+ recent_requests: Recent Requests
+ view_all_requests: View all
+ suggested_matches_card:
+ for_your_latest_offer: For your latest offer
+ for_your_latest_request: For your latest request
+ no_suggestions: No suggestions yet — create an offer or request to see matches.
+ suggested_matches: Suggested Matches
+ infrastructure:
+ locations: Locations
+ invitations:
+ accept: Accept
+ confirm_remove: Are you sure you want to remove this invitation?
+ decline: Decline
+ event_name: 'Event:'
+ event_panel:
+ title: Invite People
+ external_user: External user
+ invite_by_email: Invite by Email
+ invite_person: Invite Member
+ invitee: Invitee
+ invitee_email: Email
+ invitee_type: Invitee Type
+ login_to_respond: Please log in to respond to your invitation.
+ pending: Pending Invitations
+ register_to_respond: Please register to respond to your invitation.
+ review: Invitation
+ search_people: Search for people...
+ select_person: Select Person
+ send_invite: Send Invitation
+ title: Event Invitations
+ type:
+ email: Email
+ person: Member
+ joatu:
+ agreements:
+ accept: Accept
+ create:
+ success: Success
+ index:
+ title: Agreements
+ offer: Offer
+ offer_creator: Offer Creator
+ participants: Participants
+ reject: Reject
+ request: Request
+ request_creator: Request Creator
+ show:
+ details: Details
+ title: Agreement
+ status: Status
+ by: by
+ categories:
+ index:
+ new_btn_text: New Category
+ help:
+ agreements:
+ index: Agreements connect one Offer and one Request. Use this list to review
+ or manage your agreements.
+ show: This page connects one Offer and one Request. If you are involved,
+ you can accept or reject to confirm the plan.
+ hide: Hide this help
+ offers:
+ form: Tell people what you can offer. A clear title and a few details go
+ a long way. Choose one or more categories so the right people can find
+ you.
+ index: Browse offers. Use search and filters to find what you need. Click
+ an offer to see details.
+ show: This is an offer. If it fits your needs, you can start a conversation
+ or create an agreement from a matching request.
+ requests:
+ form: Tell people what you need. A clear title and a few details help others
+ understand. Pick one or more categories so helpers can find you.
+ index: Browse requests. Use search and filters to find ways to help. Click
+ a request to see details.
+ show: This is a request. If it matches your offer, you can start a conversation
+ or create an agreement from a matching offer.
+ screenshot_alt: Help screenshot
+ show_again: Show help again
+ hub:
+ empty_agreements: No agreements yet.
+ empty_offers: No offers yet.
+ empty_requests: No requests yet.
+ my_agreements: My Agreements
+ my_offers: My Offers
+ my_requests: My Requests
+ sign_in_hint: Sign in to see your offers, requests, and agreements.
+ suggested: Suggested Matches
+ title: Exchange Hub
+ view_all_agreements: View all agreements
+ view_all_offers: View all offers
+ view_all_requests: View all requests
+ matches:
+ create_agreement: Create Agreement
+ matches_offer: 'Matches offer:'
+ matches_request: 'Matches request:'
+ none: No matches found right now.
+ title: Potential Matches
+ nav:
+ agreements: Agreements
+ hub: Hub
+ manage_categories: Manage Categories
+ offers: Offers
+ requests: Requests
+ offers:
+ agreement_heading: Agreement
+ agreement_with: Agreement with
+ back_to_offers: Back to Offers
+ edit_offer: Edit Offer
+ empty: No offers found
+ index_header: Offers
+ new_offer: New Offer
+ response_to_your_offer_heading: Response to your Offer
+ save_offer: Save Offer
+ source_request_heading: Responding to Request
+ view_agreement: View Agreement
+ view_offer: View Offer
+ your_response_heading: Your Response
+ requests:
+ agreement_heading: Agreement
+ back_to_requests: Back to Requests
+ edit_request: Edit Request
+ empty: No requests found
+ index_header: Requests
+ new_request: New Request
+ respond_with_offer: Respond with Offer
+ response_to_your_request_heading: Response to your Request
+ save_request: Save Request
+ source_offer_heading: Responding to Offer
+ view_agreement: View Agreement
+ view_request: View Request
+ your_response_heading: Your Response
+ respond_with_request: Respond with Request
+ response_to_your: Response to your %{type}
+ responses:
+ in_response_to: In response to
+ search:
+ labels:
+ filter_by_type: Filter by category
+ order_by: Order by
+ per_page: Per page
+ search-term: Search term
+ status: Status
+ submit: Search
+ newest: Newest
+ oldest: Oldest
+ placeholders:
+ search: Search by title or description
+ title: Search
+ status:
+ any: Any
+ closed: Closed
+ open: Open
+ type:
+ offer: Offer
+ request: Request
+ you: You
+ joatu_mailer:
+ agreement_created:
+ greeting: Hello %{recipient_name},
+ message_intro: An agreement has been created between "%{offer_name}" and "%{request_name}".
+ signature: |-
+ Best regards,
+ The %{platform} Team
+ signature_html: |-
+ Best regards,
+ The %{platform} Team
+ subject: A new agreement was created
+ labelable:
+ custom-label-placeholder: Your custom label
+ loading: Loading...
+ mailer:
+ footer:
+ no_reply: This is an automated message. Please do not reply to this email.
+ messages:
+ form:
+ placeholder: Type your message...
+ send: Send
+ metrics:
+ shares:
+ invalid_parameters: Invalid parameters for share tracking.
+ shares_controller:
+ error_tracking: 'Error tracking share:'
+ failed_tracking: Failed to track share.
+ ga_not_initialized: Google Analytics not initialized.
+ unsupported_platform: 'Unsupported platform: %{platform}'
+ mobile_bar:
+ conversations: Conversations
+ navigation: Navigation
+ profile: Profile
+ models:
+ address_missing_type: Address must be physical, postal, or both.
+ ends_at_before_starts_at: must be after the start time
+ host_single: can only be set for a single record
+ protected_destroy: This record is protected and cannot be deleted.
+ navigation_areas:
+ edit:
+ title: Edit Navigation Area
+ index:
+ items: Items
+ new_navigation_area: New navigation area
+ new:
+ title: New Navigation Area
+ show:
+ new_navigation_item: New navigation item
+ navigation_items:
+ edit:
+ title: 'Edit Navigation Item: %{title}'
+ form:
+ dynamic_route: Dynamic Route
+ enter_nav_item_title: Enter the nav item title.
+ link_to_page: Link to Page
+ parent_item: Parent Item
+ index:
+ new_navigation_item: New Navigation Item
+ title: Navigation Items for %{name}
+ new:
+ title: New Navigation Item for %{name}
+ route_names:
+ calendars: Calendars
+ calls_for_interest: Calls for Interest
+ communities: Communities
+ content_blocks: Content Blocks
+ events: Events
+ geography_continents: Continents
+ geography_countries: Countries
+ geography_maps: Maps
+ geography_regions: Regions
+ geography_settlements: Settlements
+ geography_states: States
+ host_dashboard: Host Dashboard
+ joatu_offers: Offers
+ joatu_requests: Requests
+ metrics_reports: Metrics Reports
+ navigation_areas: Navigation Areas
+ pages: Pages
+ people: People
+ platforms: Platforms
+ resource_permissions: Resource Permissions
+ roles: Roles
+ users: Users
+ new_message_notifier:
+ from_html: 'From: %{sender}'
+ new_message: New Message
+ not_found:
+ description: The page you are looking for might have been removed, had its name
+ changed, or is temporarily unavailable.
+ title: 404 - Page Not Found
+ notifications:
+ event_invitation:
+ body: Invitation to %{event_name}
+ title: You have been invited to an event
+ event_reminder:
+ body_1h: "%{event_name} starts in 1 hour at %{starts_at}"
+ body_24h: "%{event_name} starts tomorrow at %{starts_at}"
+ body_generic: 'Reminder: %{event_name} starts at %{starts_at}'
+ title: 'Reminder: %{event_name}'
+ event_update:
+ body: "%{event_name} has been updated: %{changes}"
+ title: 'Event updated: %{event_name}'
+ index:
+ no_notifications: You have no notifications.
+ notifications: Notifications
+ joatu:
+ agreement_created:
+ content: An agreement between "%{offer}" and "%{request}" has been created.
+ title: Agreement created
+ mark_all_as_read: Mark all as read
+ mark_as_read: Mark as read
+ new: New
+ new_message:
+ content: "%{content}"
+ title: "%{sender}: %{conversation}"
+ no_notifications: No notifications
+ time_ago: "%{time} ago"
+ page_authorship_notifier:
+ added: You were added as an author on %{page_title}
+ added_by: "%{actor_name} added you as an author on %{page_title}"
+ removed: You were removed as an author on %{page_title}
+ removed_by: "%{actor_name} removed you as an author on %{page_title}"
+ pages:
+ form:
+ create_page_before_adding_content: Create page before adding content
+ index:
+ clear_filter: Clear filter
+ clear_filters: Clear Filters
+ filter: Filter
+ new_page: New page
+ search_by_slug: Search by slug...
+ search_by_title: Search by title...
+ people:
+ allow_messages_from_members: Allow messages from platform members
+ calendar:
+ attended: Attended
+ no_events: No events in calendar
+ no_events_description: Events will appear here when you RSVP as "Going" to
+ events.
+ no_past_events: No past events attended.
+ no_upcoming_events: No upcoming events.
+ recent_past_events: Recent Past Events
+ title: Personal Calendar
+ upcoming_events: Upcoming Events
+ device_permissions:
+ camera: Camera
+ location: Location
+ microphone: Microphone
+ notifications: Notifications
+ permissions_group_label: Permissions group label
+ index:
+ new_person: New person
+ notification_preferences: Message notification preferences
+ submit:
+ save: Save
+ tabs:
+ contact_details: Contact Details
+ details: Details
+ device-permissions: Device-permissions
+ images: Images
+ preferences: Preferences
+ person_block:
+ cannot_block_manager: no puede ser un administrador de la plataforma
+ cannot_block_self: no puedes bloquearte a ti mismo
+ person_blocks:
+ index:
+ actions: Actions
+ block_person: Block Person
+ blocked_at: Blocked
+ blocked_count:
+ few: Заблоковано %{count} особи
+ many: Заблоковано %{count} осіб
+ one: Заблоковано 1 особу
+ other: Заблоковано %{count} осіб
+ zero: Нікого не заблоковано
+ blocked_on: Blocked on %{date}
+ no_blocked_people: You haven't blocked anyone yet.
+ no_blocked_people_description: When you block someone, they won't be able
+ to interact with you.
+ search: Search
+ search_placeholder: Search blocked people...
+ title: Blocked People
+ unblock: Unblock
+ unblock_confirmation: Are you sure you want to unblock this person?
+ new:
+ back_to_list: Back to Blocked People
+ blocked_person_help: Select a person you want to block from the search results.
+ blocked_person_label: Person to Block
+ blocked_person_placeholder: Search for a person to block...
+ blocked_person_prompt: Select a person to block
+ cancel: Cancel
+ submit: Block Person
+ title: Block Person
+ notices:
+ already_blocked: This person is already blocked.
+ blocked: Person has been blocked successfully.
+ not_found: Person not found.
+ platform_manager_error: Platform managers cannot be blocked.
+ self_block_error: You cannot block yourself.
+ unblocked: Person has been unblocked successfully.
+ phone_numbers:
+ add: Add Phone Number
+ label: Label
+ labels:
+ fax: Fax
+ home: Home
+ mobile: Mobile
+ other: Other
+ work: Work
+ number: Номер
+ title: Номери телефонів
+ platform_invitation:
+ acceptance_html: Реєструючи обліковий запис користувача нижче, ви приймаєте
+ це запрошення приєднатися до %{platform}.
+ community_role_html: Вам надано роль %{role} в спільноті %{platform}.
+ greeting-placeholder: Було приємно познайомитися з вами нещодавно. Ось запрошення,
+ яке я обіцяв вам надіслати!
+ hints:
+ type: Виберіть тип запрошення.
+ platform_role_html: Вам надано роль %{role} на платформі %{platform}.
+ title: Вас запрошують приєднатися до %{platform}
+ platform_invitation_mailer:
+ invite:
+ greeting: Привіт %{recipient_name},
+ ignore: Якщо ви не очікували цього запрошення, будь ласка, проігноруйте цей
+ електронний лист.
+ invitation_code: 'Код запрошення: %{invitation_code}'
+ link_text: Прийняти запрошення
+ message: 'Вас запрошено приєднатися до %{platform} на нашій платформі. Щоб
+ прийняти запрошення, будь ласка, натисніть посилання нижче:'
+ signature_html: З найкращими побажаннями,
Команда %{platform}
+ subject: Вас запрошують приєднатися до %{platform}!
+ valid_from: Це запрошення дійсне з %{valid_from}.
+ valid_period: Це запрошення дійсне з %{valid_from} до %{valid_until}.
+ platform_invitations:
+ create_new_invitation: Створити нове запрошення
+ filter_by_status: Фільтрувати за статусом
+ invitations: Запрошення
+ new_invitation: Нове запрошення
+ search_by_email: Пошук за електронною поштою
+ status:
+ accepted: Прийнято
+ expired: Закінчився термін
+ pending: Очікує
+ platforms:
+ show:
+ new_invitation: Нове запрошення
+ posts:
+ authors: Автори
+ back_to_posts: Назад до постів
+ create_post: Створити перший пост
+ created_on: Створено %{date}
+ created_on_short: Створено
+ delete: Видалити запис
+ edit: Редагувати пост
+ edit_post: Редагувати пост
+ index:
+ new_btn_text: Новий пост
+ labels:
+ privacy: Конфіденційність
+ new_post: Новий пост
+ none_body: Наразі немає постів для показу.
+ none_title: Постів поки що немає
+ published_on: Опубліковано %{date}
+ save_post: Зберегти пост
+ updated_on_short: Оновлено
+ view_post: Переглянути пост
+ primary: Основний
+ registrations:
+ captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, спробуйте
+ знову.
+ remove: Видалити
+ resource_permissions:
+ index:
+ new_resource_permission: Новий дозвіл ресурсу
+ roles:
+ index:
+ new_role: Нова роль
+ search:
+ all: Все
+ page_title: Результати пошуку для "%{query}"
+ select: Вибрати
+ settings:
+ index:
+ account:
+ coming_soon: Налаштування облікового запису з'являться незабаром.
+ description: Оновіть свої облікові дані та налаштування безпеки.
+ title: Налаштування облікового запису
+ blocked_people:
+ description: Керуйте користувачами, яких ви заблокували від взаємодії з
+ вами.
+ title: Заблоковані люди
+ navigation_aria_label: Навігація налаштувань
+ personal:
+ coming_soon: Особисті налаштування з'являться незабаром.
+ description: Керуйте своєю особистою інформацією та налаштуваннями.
+ title: Особисті налаштування
+ platform:
+ coming_soon: Налаштування платформи з'являться незабаром.
+ description: Налаштуйте загальноплатформні параметри та налаштування.
+ title: Налаштування платформи
+ privacy:
+ coming_soon: Privacy settings are coming soon.
+ description: Control your privacy and data sharing preferences.
+ title: Privacy Settings
+ tabs:
+ account: Account
+ blocked_people: Blocked People
+ personal: Personal
+ platform: Platform
+ privacy: Privacy
+ title: Settings
+ share_buttons:
+ aria_label: Share on %{platform}
+ bluesky: Share on Bluesky
+ default_title: Check this out!
+ facebook: Share on Facebook
+ linkedin: Share on LinkedIn
+ pinterest: Pin
+ reddit: Share on Reddit
+ share: Share
+ whatsapp: Share on WhatsApp
+ shared:
+ translated_field_hints:
+ description: Short description (at least one locale must be filled)
+ description_html: Rich-text description (supports formatting; at least one
+ locale must be filled)
+ name: Enter a name (at least one locale must be filled)
+ slug: URL‑friendly identifier (unique across locales)
+ unpublished: Unpublished
+ social_media_accounts:
+ add: Додати акаунт соціальних мереж
+ handle: Нікнейм
+ platform: Платформа
+ title: Акаунти соціальних мереж
+ url: URL-адреса
+ website_links:
+ add: Додати посилання на веб-сайт
+ labels:
+ about_us: Про нас
+ blog: Блог
+ careers: Кар'єра
+ community_page: Сторінка спільноти
+ company_website: Веб-сайт компанії
+ contact_us: Зв'яжіться з нами
+ documentation: Документація
+ donations: Пожертви
+ events: Події
+ faq: ЧаПи
+ forum: Форум
+ newsletter: Розсилка
+ other: Інше
+ personal_website: Особистий веб-сайт
+ portfolio: Портфоліо
+ privacy_policy: Політика конфіденційності
+ product_page: Сторінка продукту
+ resume: Резюме
+ services: Послуги
+ support: Підтримка
+ terms_of_service: Умови використання
+ title: Посилання на веб-сайти
+ block: :activerecord.models.block
+ community:
+ create_failed: Створення не вдалося
+ created: Створено
+ update_failed: Оновлення не вдалося
+ updated: Оновлено
+ content: Зміст
+ conversation: :activerecord.models.conversation
+ conversation_participants: Учасники розмови
+ conversations:
+ errors:
+ no_permitted_participants: You can only add platform managers or members who
+ have opted in to receive messages.
+ date:
+ abbr_day_names:
+ - Нд
+ - Пн
+ - Вт
+ - Ср
+ - Чт
+ - Пт
+ - Сб
+ abbr_month_names:
+ -
+ - Січ
+ - Лют
+ - Бер
+ - Кві
+ - Тра
+ - Чер
+ - Лип
+ - Сер
+ - Вер
+ - Жов
+ - Лис
+ - Гру
+ day_names:
+ - Неділя
+ - Понеділок
+ - Вівторок
+ - Середа
+ - Четвер
+ - П'ятниця
+ - Субота
+ formats:
+ default: "%Y-%m-%d"
+ long: "%B %d, %Y"
+ short: "%b %d"
+ month_names:
+ -
+ - Січень
+ - Лютий
+ - Березень
+ - Квітень
+ - Травень
+ - Червень
+ - Липень
+ - Серпень
+ - Вересень
+ - Жовтень
+ - Листопад
+ - Грудень
+ order:
+ - :year
+ - :month
+ - :day
+ datetime:
+ distance_in_words:
+ about_x_hours:
+ few: близько %{count} годин
+ many: близько %{count} годин
+ one: близько години
+ other: близько %{count} годин
+ about_x_months:
+ few: близько %{count} місяців
+ many: близько %{count} місяців
+ one: близько місяця
+ other: близько %{count} місяців
+ about_x_years:
+ few: близько %{count} років
+ many: близько %{count} років
+ one: близько року
+ other: близько %{count} років
+ almost_x_years:
+ few: майже %{count} років
+ many: майже %{count} років
+ one: майже рік
+ other: майже %{count} років
+ half_a_minute: півхвилини
+ less_than_x_minutes:
+ few: менше %{count} хвилин
+ many: менше %{count} хвилин
+ one: менше хвилини
+ other: менше %{count} хвилин
+ less_than_x_seconds:
+ few: менше %{count} секунд
+ many: менше %{count} секунд
+ one: менше секунди
+ other: менше %{count} секунд
+ over_x_years:
+ few: понад %{count} років
+ many: понад %{count} років
+ one: понад рік
+ other: понад %{count} років
+ x_days:
+ few: "%{count} дні"
+ many: "%{count} днів"
+ one: день
+ other: "%{count} днів"
+ x_minutes:
+ few: "%{count} хвилини"
+ many: "%{count} хвилин"
+ one: хвилина
+ other: "%{count} хвилин"
+ x_months:
+ few: "%{count} місяці"
+ many: "%{count} місяців"
+ one: місяць
+ other: "%{count} місяців"
+ x_seconds:
+ few: "%{count} секунди"
+ many: "%{count} секунд"
+ one: секунда
+ other: "%{count} секунд"
+ x_years:
+ few: "%{count} роки"
+ many: "%{count} років"
+ one: рік
+ other: "%{count} років"
+ prompts:
+ day: День
+ hour: Година
+ minute: Хвилина
+ month: Місяць
+ second: Секунда
+ year: Рік
+ devise:
+ confirmations:
+ confirmed: Вашу адресу електронної пошти було успішно підтверджено.
+ new:
+ email:
+ label: Електронна пошта
+ resend_confirmation_instructions: Повторно надіслати інструкції підтвердження
+ send_instructions: Ви отримаєте електронний лист з інструкціями щодо підтвердження
+ вашої адреси електронної пошти через кілька хвилин.
+ send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі
+ даних, ви отримаєте електронний лист з інструкціями щодо підтвердження вашої
+ адреси електронної пошти через кілька хвилин.
+ failure:
+ already_authenticated: Ви вже увійшли до системи.
+ inactive: Ваш обліковий запис ще не активовано.
+ invalid: Неправильний %{authentication_keys} або пароль.
+ last_attempt: У вас є ще одна спроба до блокування вашого облікового запису.
+ locked: Ваш обліковий запис заблоковано.
+ not_found_in_database: Неправильний %{authentication_keys} або пароль.
+ timeout: Ваша сесія закінчилася. Будь ласка, увійдіть знову, щоб продовжити.
+ unauthenticated: Вам потрібно увійти або зареєструватися, щоб продовжити.
+ unconfirmed: Вам потрібно підтвердити свою адресу електронної пошти, щоб продовжити.
+ mailer:
+ confirmation_instructions:
+ action: Підтвердити мій обліковий запис
+ greeting: Ласкаво просимо %{recipient}!
+ instruction: 'Ви можете підтвердити електронну пошту свого облікового запису
+ за посиланням нижче:'
+ subject: Інструкції підтвердження
+ email_changed:
+ greeting: Привіт %{recipient}!
+ message: Ми звертаємося до вас, щоб повідомити, що вашу електронну пошту було
+ змінено на %{email}.
+ message_unconfirmed: Ми звертаємося до вас, щоб повідомити, що вашу електронну
+ пошту змінюється на %{email}.
+ subject: Електронну пошту змінено
+ password_change:
+ greeting: Привіт %{recipient}!
+ message: Ми звертаємося до вас, щоб повідомити, що ваш пароль було змінено.
+ subject: Пароль змінено
+ reset_password_instructions:
+ action: Змінити мій пароль
+ greeting: Привіт %{recipient}!
+ instruction: Хтось запросив посилання для зміни вашого пароля. Ви можете зробити
+ це за посиланням нижче.
+ instruction_2: Якщо ви не запитували це, будь ласка, ігноруйте цей електронний
+ лист.
+ instruction_3: Ваш пароль не зміниться, поки ви не перейдете за посиланням
+ вище і не створите новий.
+ subject: Інструкції скидання пароля
+ unlock_instructions:
+ action: Розблокувати мій обліковий запис
+ greeting: Привіт %{recipient}!
+ instruction: 'Натисніть посилання нижче, щоб розблокувати свій обліковий запис:'
+ message: Ваш обліковий запис було заблоковано через надмірну кількість невдалих
+ спроб входу.
+ subject: Інструкції розблокування
+ omniauth_callbacks:
+ failure: Не вдалося автентифікувати вас з %{kind} через "%{reason}".
+ success: Успішно автентифіковано з облікового запису %{kind}.
+ passwords:
+ edit:
+ change_my_password: Змінити мій пароль
+ change_your_password: Змінити ваш пароль
+ confirm_new_password: Підтвердити новий пароль
+ new_password: Новий пароль
+ new:
+ email:
+ help: Введіть вашу адресу електронної пошти для отримання інструкцій зі
+ скидання.
+ label: Електронна пошта
+ forgot_your_password: Забули свій пароль?
+ send_me_reset_password_instructions: Надіслати мені інструкції зі скидання
+ пароля
+ no_token: Ви не можете отримати доступ до цієї сторінки, не перейшовши з електронного
+ листа для скидання пароля. Якщо ви перейшли з електронного листа для скидання
+ пароля, будь ласка, переконайтеся, що використали повну надану URL-адресу.
+ send_instructions: Ви отримаєте електронний лист з інструкціями щодо скидання
+ вашого пароля через кілька хвилин.
+ send_paranoid_instructions: Якщо ваша адреса електронної пошти є в нашій базі
+ даних, ви отримаєте посилання для відновлення пароля на вашу адресу електронної
+ пошти через кілька хвилин.
+ updated: Ваш пароль було успішно змінено. Тепер ви увійшли до системи.
+ updated_not_active: Ваш пароль було успішно змінено.
+ registrations:
+ destroyed: До побачення! Ваш обліковий запис було успішно скасовано. Сподіваємося
+ побачити вас знову найближчим часом.
+ edit:
+ are_you_sure: Ви впевнені?
+ cancel_my_account: Скасувати мій обліковий запис
+ currently_waiting_confirmation_for_email: 'Очікує підтвердження для: %{email}'
+ edit_again: Редагувати деталі облікового запису
+ leave_blank_if_you_don_t_want_to_change_it: залиште порожнім, якщо не хочете
+ змінювати
+ title: Редагувати %{resource}
+ unhappy: Незадоволені?
+ update: Оновити
+ we_need_your_current_password_to_confirm_your_changes: нам потрібен ваш поточний
+ пароль для підтвердження змін
+ new:
+ agreements:
+ privacy_policy: Я погоджуюся з Політикою конфіденційності
+ terms_of_service: Я погоджуюся з Умовами використання
+ agreements_must_accept: Ви повинні прийняти Умови використання та Політику
+ конфіденційності для продовження.
+ agreements_required: Ви повинні прийняти Політику конфіденційності та Умови
+ використання
+ captcha_validation_failed: Помилка перевірки безпеки. Будь ласка, пройдіть
+ перевірку безпеки та спробуйте знову.
+ code_of_conduct:
+ label: Я погоджуюся з Кодексом поведінки
+ email:
+ help: Будь ласка, введіть дійсну адресу електронної пошти.
+ label: Електронна пошта
+ invitation_code: Код запрошення
+ invitation_code_help_html: Платформа %{platform} вимагає
+ запрошення перед реєстрацією. Якщо вас було запрошено, будь ласка, введіть
+ ваш код запрошення в поле нижче для доступу до форми реєстрації.
+ invitation_required: Потрібне запрошення
+ password:
+ help: Використовуйте сильний пароль для безпеки вашого облікового запису.
+ label: Пароль
+ password_confirmation:
+ help: Будь ласка, підтвердіть ваш пароль.
+ label: Підтвердження пароля
+ person:
+ description: Опис
+ description_hint: Надайте короткий опис себе, щоб допомогти іншим краще
+ вас пізнати.
+ identifier: Ім'я користувача
+ identifier_hint_html: Ваш ідентифікатор - це унікальне ім'я користувача,
+ що ідентифікує ваш профіль на сайті %{platform}.
+ name: Ім'я
+ name_hint: Будь ласка, надайте ваше повне ім'я.
+ privacy_policy:
+ label: Я погоджуюся з Політикою конфіденційності
+ profile_details: Деталі профілю
+ sign_up: Зареєструватися
+ submit: Надіслати
+ terms_of_service:
+ label: Я погоджуюся з Умовами використання
+ view: Переглянути
+ signed_up: Ласкаво просимо! Ви успішно зареєструвалися.
+ signed_up_but_inactive: Ви успішно зареєструвалися. Проте ми не змогли авторизувати
+ вас, тому що ваш обліковий запис ще не активовано.
+ signed_up_but_locked: Ви успішно зареєструвалися. Проте ми не змогли авторизувати
+ вас, тому що ваш обліковий запис заблоковано.
+ signed_up_but_unconfirmed: Повідомлення з посиланням підтвердження було надіслано
+ на вашу адресу електронної пошти. Будь ласка, перейдіть за посиланням для
+ активації вашого облікового запису.
+ update_needs_confirmation: Ви успішно оновили свій обліковий запис, але нам
+ потрібно перевірити вашу нову адресу електронної пошти. Будь ласка, перевірте
+ свою електронну пошту та перейдіть за посиланням підтвердження, щоб підтвердити
+ вашу нову адресу електронної пошти.
+ updated: Ваш обліковий запис було успішно оновлено.
+ updated_but_not_signed_in: Ваш обліковий запис було успішно оновлено, але оскільки
+ ваш пароль було змінено, вам потрібно увійти знову.
+ sessions:
+ already_signed_out: Ви вже вийшли з системи.
+ new:
+ email:
+ help: Введіть вашу адресу електронної пошти для входу.
+ label: Електронна пошта
+ password:
+ help: Введіть ваш пароль для входу.
+ label: Пароль
+ toggle: Показати/Приховати пароль
+ remember_me: Запам'ятати мене
+ sign_in: Увійти
+ signed_in: Ви успішно увійшли до системи.
+ signed_out: Ви успішно вийшли з системи.
+ shared:
+ links:
+ back: Назад
+ didn_t_receive_confirmation_instructions: Не отримали інструкції підтвердження?
+ didn_t_receive_unlock_instructions: Не отримали інструкції розблокування?
+ forgot_your_password: Забули свій пароль?
+ sign_in: Увійти
+ sign_in_with_provider: Увійти через %{provider}
+ sign_up: Зареєструватися
+ minimum_password_length: "(мінімум %{count} символів)"
+ unlocks:
+ new:
+ resend_unlock_instructions: Повторно надіслати інструкції розблокування
+ send_instructions: Ви отримаєте електронний лист з інструкціями щодо розблокування
+ вашого облікового запису через кілька хвилин.
+ send_paranoid_instructions: Якщо ваш обліковий запис існує, ви отримаєте електронний
+ лист з інструкціями щодо його розблокування через кілька хвилин.
+ unlocked: Ваш обліковий запис було успішно розблоковано. Будь ласка, увійдіть
+ для продовження.
+ errors:
+ format: "%{attribute} %{message}"
+ internal_server_error:
+ description: Вибачте, але щось пішло не так з нашого боку.
+ title: 500 - Внутрішня помилка сервера
+ messages:
+ accepted: має бути прийнято
+ already_confirmed: вже підтверджено, будь ласка, спробуйте увійти
+ blank: не може бути пустим
+ confirmation: не співпадає з %{attribute}
+ confirmation_period_expired: потрібно підтвердити протягом %{period}, будь ласка
+ запросіть новий
+ empty: не може бути порожнім
+ equal_to: має дорівнювати %{count}
+ even: має бути парним
+ exclusion: зарезервовано
+ expired: закінчився, будь ласка, запросіть новий
+ greater_than: має бути більше %{count}
+ greater_than_or_equal_to: має бути більше або дорівнювати %{count}
+ in: має бути в %{count}
+ inclusion: не включено до списку
+ invalid: неправильний
+ less_than: має бути менше %{count}
+ less_than_or_equal_to: має бути менше або дорівнювати %{count}
+ model_invalid: 'Перевірка не вдалася: %{errors}'
+ not_a_number: не є числом
+ not_an_integer: має бути цілим числом
+ not_found: не знайдено
+ not_locked: не було заблоковано
+ not_saved:
+ few: "%{count} помилки заборонили збереження цього %{resource}:"
+ many: "%{count} помилок заборонили збереження цього %{resource}:"
+ one: '1 помилка заборонила збереження цього %{resource}:'
+ other: "%{count} помилок заборонили збереження цього %{resource}:"
+ odd: має бути непарним
+ other_than: має бути іншим ніж %{count}
+ present: має бути порожнім
+ required: має існувати
+ taken: вже зайнято
+ too_long:
+ few: занадто довгий (максимум %{count} символи)
+ many: занадто довгий (максимум %{count} символів)
+ one: занадто довгий (максимум %{count} символ)
+ other: занадто довгий (максимум %{count} символів)
+ too_short:
+ few: занадто короткий (мінімум %{count} символи)
+ many: занадто короткий (мінімум %{count} символів)
+ one: занадто короткий (мінімум %{count} символ)
+ other: занадто короткий (мінімум %{count} символів)
+ weak_password: недостатньо сильний. Розгляньте додавання цифри, символів або
+ більше літер, щоб зробити його сильнішим. Уникайте використання слова 'password',
+ свого дня народження, або інших звичайних легко вгадуваних паролів.
+ wrong_length:
+ few: неправильна довжина (має бути %{count} символи)
+ many: неправильна довжина (має бути %{count} символів)
+ one: неправильна довжина (має бути %{count} символ)
+ other: неправильна довжина (має бути %{count} символів)
+ models:
+ address_missing_type: Адреса має бути фізичною, поштовою або обома.
+ ends_at_before_starts_at: має бути після часу початку
+ host_single: можна встановити тільки для одного запису
+ person_checklist_item:
+ directional_incomplete: Напрямок неповний
+ protected_destroy: Цей запис захищено і не може бути видалено.
+ not_found:
+ description: Сторінка, яку ви шукаєте, можливо, була видалена, змінила назву
+ або тимчасово недоступна.
+ title: 404 - Сторінку не знайдено
+ person_block:
+ cannot_block_manager: не може бути менеджером платформи
+ cannot_block_self: не можете заблокувати себе
+ template:
+ body: 'Виникли проблеми з наступними полями:'
+ header:
+ few: "%{count} помилки заборонили збереження цієї %{model}"
+ many: "%{count} помилок заборонили збереження цієї %{model}"
+ one: "%{count} помилка заборонила збереження цієї %{model}"
+ other: "%{count} помилок заборонили збереження цієї %{model}"
+ wizard:
+ max_completions: Досягнуто максимальну кількість завершень для цього майстра
+ та визначення кроку.
+ one_uncompleted: Дозволено тільки один незавершений крок на людину.
+ step_limit: Кількість завершень для цього кроку досягла максимального ліміту
+ завершень майстра.
+ event: :activerecord.models.event
+ flash:
+ checklist_item:
+ update_failed: Не вдалося оновити елемент чек-листа.
+ updated: Елемент чек-листа оновлено.
+ generic:
+ created: "%{resource} було успішно створено."
+ deleted: Видалено
+ destroyed: "%{resource} було успішно знищено."
+ error_create: Помилка створення %{resource}.
+ error_remove: Не вдалося видалити %{resource}.
+ queued: "%{resource} поставлено в чергу для відправлення."
+ removed: "%{resource} було успішно видалено."
+ unauthorized: Неавторизовано
+ updated: "%{resource} було успішно оновлено."
+ joatu:
+ agreement:
+ accepted: Угоду прийнято
+ rejected: Угоду відхилено
+ response_links:
+ offer_created: Пропозицію створено у відповідь на запит.
+ request_created: Запит створено у відповідь на пропозицію.
+ person_block:
+ blocked: Особу було успішно заблоковано.
+ unblocked: Особу було успішно розблоковано.
+ globals:
+ accepted: Прийнято
+ actions: Дії
+ add_block: Додати блок
+ add_child_item: Додати дочірній елемент
+ add_member: Додати учасника
+ ago: тому
+ all: Все
+ are_you_sure: Ви впевнені
+ back: Назад
+ back_to_list: Назад до списку
+ cancel: Скасувати
+ clear: Очистити
+ clear_filter: Очистити фільтр
+ confirm_delete: Ви впевнені, що хочете видалити цей запис?
+ create: Створити
+ declined: Відхилено
+ delete: Видалити
+ destroy: Знищити
+ draft: Чернетка
+ edit: Редагувати
+ email: Електронна пошта
+ 'false': Ні
+ forms:
+ save: Зберегти
+ from: Від
+ hidden: Приховано
+ loading: Завантаження...
+ locale: Локаль
+ message: Повідомлення
+ new: Новий
+ 'no': Ні
+ no_description: Немає опису
+ no_image: Немає зображення
+ no_invitee: Немає запрошеного
+ none: Немає
+ not_sent: Не відправлено
+ pagination:
+ of: з
+ showing: Показано
+ to: до
+ pending: Очікує
+ platform_not_public: Платформа наразі приватна. Будь ласка, увійдіть для доступу.
+ published: Опубліковано
+ remove: Видалити
+ resend: Повторно надіслати
+ save: Зберегти
+ sent: Відправлено
+ show: Показати
+ status: Статус
+ tabs:
+ about: Про
+ attendees: Учасники
+ contact: Контакт
+ events: Події
+ invitations: Запрошення
+ members: Учасники
+ to: До
+ 'true': так
+ undo_clear: Скасувати очищення
+ update: Оновити
+ url: URL
+ view: Переглянути
+ visible: Видимий
+ 'yes': Так
+ helpers:
+ close: Закрити
+ errors:
+ heading: Помилки форми
+ prohibited: Заборонено
+ hint:
+ call_for_interest:
+ cover_image: Зображення обкладинки
+ community:
+ cover_image: Зображення обкладинки
+ logo: Логотип
+ profile_image: Зображення профілю
+ event:
+ cover_image: Зображення обкладинки
+ images:
+ cover_image: Зображення обкладинки
+ person:
+ allow_messages_from_members: Дозволити отримувати повідомлення від людей,
+ окрім менеджерів платформи.
+ cover_image: Завантажте зображення обкладинки для відображення у верхній частині
+ профілю.
+ description: Надайте короткий опис або біографію.
+ locale: Виберіть бажану мову для особи.
+ name: Введіть повне ім'я особи.
+ notify_by_email: Надсилати електронний лист, коли в розмові є непрочитане
+ повідомлення.
+ profile_image: Завантажте зображення профілю для особи.
+ show_conversation_details: Показувати назву розмови та ім'я відправника в
+ сповіщеннях.
+ slug: URL-дружній ідентифікатор, зазвичай генерується автоматично.
+ required_info: Це поле обов'язкове
+ label:
+ person:
+ cover_image: Зображення обкладинки
+ description: Опис
+ name: Ім'я
+ profile_image: Зображення профілю
+ slug: Слаг
+ language_select:
+ prompt: Підказка
+ required_info: Це поле обов'язкове
+ select:
+ prompt: Будь ласка, виберіть
+ submit:
+ create: Створити %{model}
+ submit: Зберегти %{model}
+ update: Оновити %{model}
+ toolbar:
+ aria_label: Панель інструментів редактора
+ hints:
+ authors:
+ select_multiple: Оберіть одного або кількох авторів
+ categories:
+ select_multiple: Оберіть кілька категорій
+ datetime_field: Оберіть дату та час для цього поля.
+ language_select: Оберіть мову для цього поля.
+ privacy_field: Встановіть рівень конфіденційності для цього поля.
+ required_field: Це поле є обов'язковим.
+ resource:
+ type: Тип
+ type_select_field: Оберіть тип для цього поля.
+ host_dashboard:
+ index:
+ better_together: Разом краще
+ content: Контент
+ geography: Географія
+ page_title: Панель керування хоста
+ title: Панель керування хоста
+ resource_card:
+ new_resource: Новий %{resource}
+ none_yet: Поки що немає
+ total_resources: Всього ресурсів
+ view_all: Переглянути всі
+ invitations:
+ calculating: Обчислення
+ locales:
+ en: English
+ es: Español
+ fr: Français
+ uk: Українська
+ meta:
+ default_description: Ласкаво просимо на %{platform_name}
+ page:
+ description_fallback: Читайте %{title} на %{platform_name}
+ metrics:
+ search_queries:
+ invalid_parameters: Недійсні параметри для відстеження пошуку.
+ shares:
+ invalid_parameters: Недійсні параметри
+ navbar:
+ accept_invitation: Accept invitation
+ conversations: Conversations
+ host_mgmt_tooltip: Host Management
+ locale_switcher_tooltip: Change Language
+ log_out: Log Out
+ my_profile: Мій профіль
+ notifications_tooltip: Сповіщення
+ search_tooltip: Пошук
+ settings: Налаштування
+ sign_in: Увійти
+ toggle_navigation: Переключити навігацію
+ user_nav_tooltip: Меню облікового запису
+ navigation:
+ header:
+ events: Події
+ exchange_hub: Центр обміну
+ navigation_item:
+ update_failed: Оновлення не вдалося
+ updated: Оновлено
+ 'no': Ні
+ number:
+ currency:
+ format:
+ delimiter: ","
+ format: "%u%n"
+ precision: 2
+ separator: "."
+ significant: false
+ strip_insignificant_zeros: false
+ unit: "$"
+ format:
+ delimiter: ","
+ precision: 3
+ round_mode: default
+ separator: "."
+ significant: false
+ strip_insignificant_zeros: false
+ human:
+ decimal_units:
+ format: "%n %u"
+ units:
+ billion: Мільярд
+ million: Мільйон
+ quadrillion: Квадрильйон
+ thousand: Тисяча
+ trillion: Трильйон
+ unit: ''
+ format:
+ delimiter: ''
+ precision: 3
+ significant: true
+ strip_insignificant_zeros: true
+ storage_units:
+ format: "%n %u"
+ units:
+ byte:
+ few: Байти
+ many: Байт
+ one: Байт
+ other: Байт
+ eb: ЕБ
+ gb: ГБ
+ kb: КБ
+ mb: МБ
+ pb: ПБ
+ tb: ТБ
+ percentage:
+ format:
+ delimiter: ''
+ format: "%n%"
+ precision:
+ format:
+ delimiter: ''
+ og:
+ default_description: Ласкаво просимо на %{platform_name}
+ default_title: "%{platform_name}"
+ page:
+ description_fallback: Читайте %{title} на %{platform_name}
+ title: "%{title} | %{platform_name}"
+ pages:
+ confirm_destroy: Підтвердіть видалення
+ index:
+ clear_filter: Очистити фільтр
+ clear_filters: Очистити фільтри
+ filter: Фільтр
+ search_by_title: Пошук за назвою...
+ partners:
+ confirm_delete: Підтвердіть видалення
+ people:
+ confirm_delete: Підтвердіть видалення
+ platform_invitations:
+ confirm_delete: Підтвердіть видалення
+ platforms:
+ confirm_delete: Підтвердіть видалення
+ pundit:
+ errors:
+ create: Вам не дозволено створювати цей %{resource}.
+ default: Вам не дозволено виконувати цю дію з цим %{resource}.
+ destroy: Вам не дозволено видаляти цей %{resource}.
+ edit: Вам не дозволено редагувати цей %{resource}.
+ index: Вам не дозволено переглядати список %{resource}.
+ leave_conversation: Ви є останнім учасником розмови і не можете її покинути
+ new: Вам не дозволено створювати новий %{resource}.
+ show: Вам не дозволено переглядати цей %{resource}.
+ update: Вам не дозволено оновлювати цей %{resource}.
+ resource_permissions:
+ confirm_destroy: Підтвердіть видалення
+ resources:
+ block: Блок
+ calendar: Календар
+ checklist: Чек-лист
+ community: Спільнота
+ continent: Континент
+ country: Країна
+ download_failed: Завантаження не вдалося
+ invitation: Запрошення
+ invitation_email: Електронна пошта запрошення
+ member: Учасник
+ navigation_area: Область навігації
+ page: Сторінка
+ person: Особа
+ person_platform_membership: Членство особи на платформі
+ platform: Платформа
+ profile: Профіль
+ region: Регіон
+ region_settlement: Регіональне поселення
+ report: Звіт
+ resource_permission: Дозвіл ресурсу
+ role: Роль
+ settlement: Поселення
+ state: Штат/Область
+ user: Користувач
+ roles:
+ confirm_destroy: Підтвердіть видалення
+ search:
+ button: Пошук
+ placeholder: Пошук...
+ shared:
+ by: автор
+ links:
+ back: Назад
+ simple_calendar:
+ next: Наступний
+ previous: Попередній
+ today: Сьогодні
+ week: Тиждень
+ support:
+ array:
+ last_word_connector: " та "
+ two_words_connector: " та "
+ words_connector: ", "
+ time:
+ am: дп
+ formats:
+ dashboard_resource: "%d %b, %Y %H:%M"
+ date_picker: Вибір дати
+ datetime_picker: Вибір дати і часу
+ default: "%d %B %Y р. %-H:%M"
+ event: "%d %B %Y р. о %-H:%M"
+ event_date_time: "%d %b, %-H:%M"
+ event_date_time_with_year: "%d %b, %Y %-H:%M"
+ long: "%d %B %Y р. %-H:%M"
+ short: "%d %b %-H:%M"
+ time_only: "%-H:%M"
+ time_only_with_year: "%-H:%M %Y"
+ pm: пп
+ views:
+ buttons:
+ back: Назад
+ create_report: Створити звіт
+ download: Завантажити
+ go_home: На головну
+ new: Новий %{resource}
+ view_page: Переглянути сторінку
+ headers:
+ link_checker_reports: Звіти перевірки посилань
+ link_click_reports: Звіти кліків по посиланням
+ new_link_checker_report: Новий звіт перевірки посилань
+ new_link_click_report: Новий звіт кліків по посиланням
+ new_page_view_report: Новий звіт переглядів сторінок
+ page_view_reports: Звіти переглядів сторінок
+ labels:
+ actions: Дії
+ all: Всі
+ created_at: Створено
+ external: Зовнішній
+ file_format: Формат файлу
+ filters: Фільтри
+ from_date: З дати
+ id: ID
+ internal: Внутрішній
+ internal_filter: Внутрішній фільтр
+ no_file: Немає файлу
+ pageable_type_filter: Фільтр типу сторінки
+ please_correct: 'Будь ласка, виправте наступні помилки:'
+ protected: Захищений
+ sort_by_total_clicks: Сортувати за загальною кількістю кліків
+ sort_by_total_views: Сортувати за загальною кількістю переглядів
+ to_date: До дати
+ pagination:
+ aria_label: Навігація пагінації
+ current: "(поточна)"
+ first: Перша
+ last: Остання
+ next: Наступна
+ previous: Попередня
+ showing_entries: Показано %{from} до %{to} із %{total} записів
+ truncate: "…"
+ 'yes': Так
diff --git a/lib/better_together/engine.rb b/lib/better_together/engine.rb
index 0344ade23..617b45d5e 100644
--- a/lib/better_together/engine.rb
+++ b/lib/better_together/engine.rb
@@ -14,6 +14,7 @@
require 'devise'
require 'devise-i18n'
require 'devise/jwt'
+require 'devise_zxcvbn'
require 'elasticsearch/model'
require 'elasticsearch/rails'
require 'font-awesome-sass'
diff --git a/spec/controllers/better_together/person_blocks_controller_spec.rb b/spec/controllers/better_together/person_blocks_controller_spec.rb
index e72443ed4..3b07a885b 100644
--- a/spec/controllers/better_together/person_blocks_controller_spec.rb
+++ b/spec/controllers/better_together/person_blocks_controller_spec.rb
@@ -9,7 +9,7 @@
routes { BetterTogether::Engine.routes }
let(:locale) { I18n.default_locale }
- let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) }
+ let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) }
let(:person) { user.person }
let(:blocked_person) { create(:better_together_person) }
let(:another_person) { create(:better_together_person) }
diff --git a/spec/controllers/better_together/platform_invitations_controller_spec.rb b/spec/controllers/better_together/platform_invitations_controller_spec.rb
index f3bd509da..ec0f36749 100644
--- a/spec/controllers/better_together/platform_invitations_controller_spec.rb
+++ b/spec/controllers/better_together/platform_invitations_controller_spec.rb
@@ -9,7 +9,7 @@
routes { BetterTogether::Engine.routes }
let(:locale) { I18n.default_locale }
- let(:user) { find_or_create_test_user('platform_manager@example.test', 'password12345', :platform_manager) }
+ let(:user) { find_or_create_test_user('platform_manager@example.test', 'SecureTest123!@#', :platform_manager) }
let(:person) { user.person }
let(:platform) { create(:better_together_platform, name: 'Test Platform') }
let(:platform_slug) { platform.slug }
diff --git a/spec/controllers/better_together/users/registrations_controller_hook_spec.rb b/spec/controllers/better_together/users/registrations_controller_hook_spec.rb
index e62d8039d..e5d4237aa 100644
--- a/spec/controllers/better_together/users/registrations_controller_hook_spec.rb
+++ b/spec/controllers/better_together/users/registrations_controller_hook_spec.rb
@@ -2,7 +2,7 @@
require 'rails_helper'
-RSpec.describe BetterTogether::Users::RegistrationsController, :skip_host_setup do
+RSpec.describe BetterTogether::Users::RegistrationsController, :skip_host_setup, :user_registration do
include BetterTogether::CapybaraFeatureHelpers
routes { BetterTogether::Engine.routes }
diff --git a/spec/features/agreements/registration_consent_spec.rb b/spec/features/agreements/registration_consent_spec.rb
index ea6efe3dc..3d2f415c4 100644
--- a/spec/features/agreements/registration_consent_spec.rb
+++ b/spec/features/agreements/registration_consent_spec.rb
@@ -2,7 +2,7 @@
require 'rails_helper'
-RSpec.describe 'User registration agreements', :as_platform_manager do
+RSpec.describe 'User registration agreements', :as_platform_manager, :user_registration do
let!(:privacy_agreement) { BetterTogether::Agreement.find_by!(identifier: 'privacy_policy') }
# rubocop:enable RSpec/LetSetup
# rubocop:todo RSpec/LetSetup
@@ -13,8 +13,8 @@
visit new_user_registration_path(locale: I18n.default_locale)
fill_in 'user[email]', with: 'test@example.test'
- fill_in 'user[password]', with: 'password12345'
- fill_in 'user[password_confirmation]', with: 'password12345'
+ fill_in 'user[password]', with: 'SecureTest123!@#'
+ fill_in 'user[password_confirmation]', with: 'SecureTest123!@#'
fill_in 'user[person_attributes][name]', with: 'Test User'
fill_in 'user[person_attributes][identifier]', with: 'testuser'
fill_in 'user[person_attributes][description]', with: 'Tester'
@@ -30,8 +30,8 @@
visit new_user_registration_path(locale: I18n.default_locale)
fill_in 'user[email]', with: 'test@example.test'
- fill_in 'user[password]', with: 'password12345'
- fill_in 'user[password_confirmation]', with: 'password12345'
+ fill_in 'user[password]', with: 'SecureTest123!@#'
+ fill_in 'user[password_confirmation]', with: 'SecureTest123!@#'
fill_in 'user[person_attributes][name]', with: 'Test User'
fill_in 'user[person_attributes][identifier]', with: 'testuser'
fill_in 'user[person_attributes][description]', with: 'Tester'
diff --git a/spec/features/checklist_create_appends_spec.rb b/spec/features/checklist_create_appends_spec.rb
index faa30845b..b231fe927 100644
--- a/spec/features/checklist_create_appends_spec.rb
+++ b/spec/features/checklist_create_appends_spec.rb
@@ -5,7 +5,7 @@
RSpec.describe 'Checklist item creation appends to bottom', :js do
include ActionView::RecordIdentifier
- let(:manager) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) }
+ let(:manager) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) }
before do
ensure_essential_data!
diff --git a/spec/features/checklist_person_completion_spec.rb b/spec/features/checklist_person_completion_spec.rb
index 5da732dc6..a4c62f388 100644
--- a/spec/features/checklist_person_completion_spec.rb
+++ b/spec/features/checklist_person_completion_spec.rb
@@ -9,7 +9,7 @@
let!(:person) { create(:better_together_person, user: user) }
before do
- find_or_create_test_user('user@example.test', 'password12345', :user)
+ find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user)
capybara_login_as_user
end
diff --git a/spec/features/checklist_reorder_system_spec.rb b/spec/features/checklist_reorder_system_spec.rb
index 2b24c2aa0..5fccbf678 100644
--- a/spec/features/checklist_reorder_system_spec.rb
+++ b/spec/features/checklist_reorder_system_spec.rb
@@ -6,7 +6,7 @@
include ActionView::RecordIdentifier
# Use the standard test manager credentials (password must meet length requirements)
- let(:manager) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) }
+ let(:manager) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) }
before do
# Ensure essential data exists and log in
diff --git a/spec/features/devise/registration_spec.rb b/spec/features/devise/registration_spec.rb
index 2c989c407..bd86a6ccd 100644
--- a/spec/features/devise/registration_spec.rb
+++ b/spec/features/devise/registration_spec.rb
@@ -3,12 +3,12 @@
require 'rails_helper'
# rubocop:disable Metrics/BlockLength
-RSpec.feature 'User Registration' do
+RSpec.feature 'User Registration', :user_registration do
# Ensure you have a valid user created; using FactoryBot here
let!(:host_setup_wizard) do
BetterTogether::Wizard.find_or_create_by(identifier: 'host_setup')
end
- let!(:user) { build(:better_together_user) }
+ let!(:user) { build(:better_together_user, password: 'SecureTest123!@#', password_confirmation: 'SecureTest123!@#') }
let!(:person) { build(:better_together_person) }
let!(:privacy_agreement) do
BetterTogether::Agreement.find_or_create_by(identifier: 'privacy_policy') do |a|
@@ -40,9 +40,16 @@
scenario 'User registers successfully' do # rubocop:todo RSpec/MultipleExpectations
# rubocop:enable RSpec/MultipleExpectations
host_setup_wizard.mark_completed
+ # Debug: check that we have the right setup
+ puts "Current path before visit: #{current_path}"
+ puts "Host platform exists: #{BetterTogether::Platform.find_by(host: true).present?}"
+ puts "Wizard completed: #{host_setup_wizard.completed?}"
# byebug
# Visit the sign-in page (adjust the path if your routes differ)
visit better_together.new_user_registration_path
+ puts "Current path after visit: #{current_path}"
+ puts "Page title: #{page.title}"
+ puts "Page has Sign Up: #{page.has_content?('Sign Up')}"
# Fill in the Devise login form
fill_in 'user[email]', with: user.email
@@ -52,26 +59,66 @@
fill_in 'user[person_attributes][name]', with: person.name
fill_in 'user[person_attributes][identifier]', with: person.identifier
+ puts 'Looking for agreement checkboxes:'
+ puts " terms_of_service_agreement: #{page.has_field?('terms_of_service_agreement')}"
+ puts " privacy_policy_agreement: #{page.has_field?('privacy_policy_agreement')}"
+ puts " code_of_conduct_agreement: #{page.has_field?('code_of_conduct_agreement')}"
+
if page.has_unchecked_field?('terms_of_service_agreement')
+ puts ' Checking terms_of_service_agreement'
check 'terms_of_service_agreement'
elsif page.has_unchecked_field?('user_accept_terms_of_service')
+ puts ' Checking user_accept_terms_of_service'
check 'user_accept_terms_of_service'
end
if page.has_unchecked_field?('privacy_policy_agreement')
+ puts ' Checking privacy_policy_agreement'
check 'privacy_policy_agreement'
elsif page.has_unchecked_field?('user_accept_privacy_policy')
+ puts ' Checking user_accept_privacy_policy'
check 'user_accept_privacy_policy'
end
if page.has_unchecked_field?('code_of_conduct_agreement')
+ puts ' Checking code_of_conduct_agreement'
check 'code_of_conduct_agreement'
elsif page.has_unchecked_field?('user_accept_code_of_conduct')
+ puts ' Checking user_accept_code_of_conduct'
check 'user_accept_code_of_conduct'
end
# Click the login button (make sure the button text matches your view)
+ puts "Before clicking Sign Up - User count: #{BetterTogether::User.count}"
+ puts "Before clicking Sign Up - Page has button: #{page.has_button?('Sign Up')}"
click_button 'Sign Up'
+ puts "After clicking Sign Up - Current path: #{current_path}"
+ puts "After clicking Sign Up - User count: #{BetterTogether::User.count}"
+ puts "After clicking Sign Up - Person count: #{BetterTogether::Person.count}"
+ puts "After clicking Sign Up - Identification count: #{BetterTogether::Identification.count}"
+ created_user = BetterTogether::User.last
+ puts "Created user ID: #{created_user&.id}"
+ puts "Created user has person: #{created_user&.person.present?}"
+ puts "Person ID: #{created_user&.person&.id}" if created_user&.person
+ puts "Person persisted: #{created_user&.person&.persisted?}" if created_user&.person
+ puts "Person errors: #{created_user.person.errors.full_messages}" if created_user&.person&.errors&.any?
+ puts "Person identification exists: #{created_user&.person_identification.present?}" if created_user
+ if created_user&.person_identification
+ puts "Person identification persisted: #{created_user&.person_identification&.persisted?}"
+ end
+
+ # Check if person was created separately
+ all_persons = BetterTogether::Person.all
+ puts "All persons: #{all_persons.map(&:id)}"
+
+ # Check if there are any identifications
+ all_identifications = BetterTogether::Identification.all
+ puts "All identifications: #{all_identifications.map do |i|
+ "#{i.id} -> agent: #{i.agent_type}##{i.agent_id}, identity: #{i.identity_type}##{i.identity_id}"
+ end}"
+ puts "After clicking Sign Up - AgreementParticipant count: #{BetterTogether::AgreementParticipant.count}"
+ puts "After clicking Sign Up - PersonCommunityMembership count: #{BetterTogether::PersonCommunityMembership.count}"
+ puts "Page content after submit: #{page.body.include?('error') ? 'CONTAINS ERRORS' : 'NO ERRORS VISIBLE'}"
# Expect a confirmation message (this text may vary based on your flash messages)
# rubocop:disable Layout/LineLength
diff --git a/spec/features/invitations/platform_invitation_accept_spec.rb b/spec/features/invitations/platform_invitation_accept_spec.rb
index 27fb05b31..c0d6d0102 100644
--- a/spec/features/invitations/platform_invitation_accept_spec.rb
+++ b/spec/features/invitations/platform_invitation_accept_spec.rb
@@ -2,7 +2,7 @@
require 'rails_helper'
-RSpec.describe 'accepting a platform invitation' do
+RSpec.describe 'accepting a platform invitation', :user_registration do
let!(:invitation) do
create(:better_together_platform_invitation,
invitable: configure_host_platform)
diff --git a/spec/features/notifications/mark_as_read_spec.rb b/spec/features/notifications/mark_as_read_spec.rb
index 3847fcc9e..3d5f067f5 100644
--- a/spec/features/notifications/mark_as_read_spec.rb
+++ b/spec/features/notifications/mark_as_read_spec.rb
@@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe 'message notifications', :as_user do
- let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) }
+ let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) }
let(:person) { user.person }
let(:conversation) { create(:conversation, creator: person) }
# let!(:conversation_participant) do
diff --git a/spec/mailers/better_together/event_mailer_spec.rb b/spec/mailers/better_together/event_mailer_spec.rb
index bd5b41134..697ad4bfd 100644
--- a/spec/mailers/better_together/event_mailer_spec.rb
+++ b/spec/mailers/better_together/event_mailer_spec.rb
@@ -41,7 +41,9 @@ module BetterTogether # rubocop:todo Metrics/ModuleLength
context 'when event has location' do
it 'includes location information' do
- expect(mail.body.encoded).to include(event.location_display_name)
+ # HTML-encoded version of location name due to ERB escaping
+ expected_location = ERB::Util.html_escape(event.location_display_name)
+ expect(mail.body.encoded).to include(expected_location)
end
end
diff --git a/spec/policies/better_together/conversation_policy_spec.rb b/spec/policies/better_together/conversation_policy_spec.rb
index 09aa40c19..cedcbb50e 100644
--- a/spec/policies/better_together/conversation_policy_spec.rb
+++ b/spec/policies/better_together/conversation_policy_spec.rb
@@ -7,7 +7,7 @@
let!(:host_platform) { configure_host_platform }
- let!(:manager_user) { create(:user, :confirmed, :platform_manager, password: 'password12345') }
+ let!(:manager_user) { create(:user, :confirmed, :platform_manager, password: 'SecureTest123!@#') }
let!(:manager_person) { manager_user.person }
let!(:opted_in_person) do
@@ -26,7 +26,7 @@
end
context 'when agent is a regular member' do
- let!(:regular_user) { create(:user, :confirmed, password: 'password12345') }
+ let!(:regular_user) { create(:user, :confirmed, password: 'SecureTest123!@#') }
it 'includes platform managers and opted-in members, but not non-opted members' do
# rubocop:enable RSpec/MultipleExpectations
@@ -39,7 +39,7 @@
end
describe '#create? with participants kwarg' do
- let(:regular_user) { create(:user, :confirmed, password: 'password12345') }
+ let(:regular_user) { create(:user, :confirmed, password: 'SecureTest123!@#') }
let(:policy) { described_class.new(regular_user, BetterTogether::Conversation.new) }
it 'allows create when user present and participants are permitted' do
diff --git a/spec/requests/better_together/checklist_items_reorder_spec.rb b/spec/requests/better_together/checklist_items_reorder_spec.rb
index 32c70971a..aee5d0649 100644
--- a/spec/requests/better_together/checklist_items_reorder_spec.rb
+++ b/spec/requests/better_together/checklist_items_reorder_spec.rb
@@ -3,11 +3,14 @@
require 'rails_helper'
RSpec.describe 'ChecklistItems Reorder' do
+ include AutomaticTestConfiguration
+
let(:locale) { I18n.default_locale }
- let(:platform_manager) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) }
+ let(:platform_manager) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) }
before do
- login(platform_manager.email, 'password12345')
+ configure_host_platform
+ login(platform_manager.email, 'SecureTest123!@#')
end
# rubocop:todo RSpec/MultipleExpectations
diff --git a/spec/requests/better_together/checklists_spec.rb b/spec/requests/better_together/checklists_spec.rb
index 81cff553e..32d2a64f4 100644
--- a/spec/requests/better_together/checklists_spec.rb
+++ b/spec/requests/better_together/checklists_spec.rb
@@ -59,11 +59,11 @@
# rubocop:todo RSpec/MultipleExpectations
it 'allows creator to update their checklist' do # rubocop:todo RSpec/MultipleExpectations
# rubocop:enable RSpec/MultipleExpectations
- user = create(:better_together_user, :confirmed, password: 'password12345')
+ user = create(:better_together_user, :confirmed, password: 'SecureTest123!@#')
checklist = create(:better_together_checklist, creator: user.person)
# sign in as that user
- login(user.email, 'password12345')
+ login(user.email, 'SecureTest123!@#')
patch better_together.checklist_path(checklist, locale: I18n.default_locale),
params: { checklist: { title_en: 'Creator Update' } }
diff --git a/spec/requests/better_together/conversation_message_protection_spec.rb b/spec/requests/better_together/conversation_message_protection_spec.rb
index 7a21ae7fb..e232308a0 100644
--- a/spec/requests/better_together/conversation_message_protection_spec.rb
+++ b/spec/requests/better_together/conversation_message_protection_spec.rb
@@ -11,11 +11,12 @@
configure_host_platform
# Setup: create a manager user (owner of the conversation) and another user
- manager_user = create(:user, :confirmed, :platform_manager, email: 'owner@example.test', password: 'password12345')
- other_user = create(:user, :confirmed, email: 'attacker@example.test', password: 'password12345')
+ manager_user = create(:user, :confirmed, :platform_manager, email: 'owner@example.test',
+ password: 'SecureTest123!@#')
+ other_user = create(:user, :confirmed, email: 'attacker@example.test', password: 'SecureTest123!@#')
# Create a conversation as the manager with a nested message
- login(manager_user.email, 'password12345')
+ login(manager_user.email, 'SecureTest123!@#')
post better_together.conversations_path(locale: I18n.default_locale), params: {
conversation: {
@@ -34,7 +35,7 @@
# Now sign in as other_user and attempt to change manager's message via PATCH
logout
- login(other_user.email, 'password12345')
+ login(other_user.email, 'SecureTest123!@#')
patch better_together.conversation_path(conversation, locale: I18n.default_locale), params: {
conversation: {
diff --git a/spec/requests/better_together/conversations_create_with_message_spec.rb b/spec/requests/better_together/conversations_create_with_message_spec.rb
index dd3a7bb54..bed471c3a 100644
--- a/spec/requests/better_together/conversations_create_with_message_spec.rb
+++ b/spec/requests/better_together/conversations_create_with_message_spec.rb
@@ -10,9 +10,9 @@
# Ensure the test user exists and is confirmed
unless BetterTogether::User.find_by(email: 'user@example.test')
create(:user, :confirmed, email: 'user@example.test',
- password: 'password12345')
+ password: 'SecureTest123!@#')
end
- login('user@example.test', 'password12345')
+ login('user@example.test', 'SecureTest123!@#')
end
# rubocop:todo RSpec/MultipleExpectations
diff --git a/spec/requests/better_together/conversations_request_spec.rb b/spec/requests/better_together/conversations_request_spec.rb
index dd44dcbf0..76526e36c 100644
--- a/spec/requests/better_together/conversations_request_spec.rb
+++ b/spec/requests/better_together/conversations_request_spec.rb
@@ -6,7 +6,7 @@
include RequestSpecHelper
let!(:manager_user) do
- create(:user, :confirmed, :platform_manager, email: 'manager1@example.test', password: 'password12345')
+ create(:user, :confirmed, :platform_manager, email: 'manager1@example.test', password: 'SecureTest123!@#')
end
let!(:opted_in_person) do
create(:better_together_person, preferences: { receive_messages_from_members: true }, name: 'Opted In User')
diff --git a/spec/requests/better_together/event_invitation_token_processing_spec.rb b/spec/requests/better_together/event_invitation_token_processing_spec.rb
index 5ec1e3796..0ea5851fa 100644
--- a/spec/requests/better_together/event_invitation_token_processing_spec.rb
+++ b/spec/requests/better_together/event_invitation_token_processing_spec.rb
@@ -11,7 +11,7 @@
existing
end
end
- let!(:manager_user) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) }
+ let!(:manager_user) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) }
let!(:event) do
BetterTogether::Event.create!(
diff --git a/spec/requests/better_together/event_invitations_spec.rb b/spec/requests/better_together/event_invitations_spec.rb
index d127884ad..897177608 100644
--- a/spec/requests/better_together/event_invitations_spec.rb
+++ b/spec/requests/better_together/event_invitations_spec.rb
@@ -122,11 +122,11 @@
# Ensure user exists and logged in as regular user
user = BetterTogether::User.find_by(email: 'user@example.test') ||
- create(:better_together_user, :confirmed, email: 'user@example.test', password: 'password12345')
+ create(:better_together_user, :confirmed, email: 'user@example.test', password: 'SecureTest123!@#')
# Clear any existing session and login as the specific user
logout if respond_to?(:logout)
- login(user.email, 'password12345')
+ login(user.email, 'SecureTest123!@#')
post better_together.accept_invitation_path(invitation.token, locale: locale)
@@ -148,11 +148,11 @@
valid_from: Time.current)
user = BetterTogether::User.find_by(email: 'user@example.test') ||
- create(:better_together_user, :confirmed, email: 'user@example.test', password: 'password12345')
+ create(:better_together_user, :confirmed, email: 'user@example.test', password: 'SecureTest123!@#')
# Clear any existing session and login as the specific user
logout if respond_to?(:logout)
- login(user.email, 'password12345')
+ login(user.email, 'SecureTest123!@#')
post better_together.decline_invitation_path(invitation.token, locale: locale)
diff --git a/spec/requests/better_together/events_controller_spec.rb b/spec/requests/better_together/events_controller_spec.rb
index 202f45ba1..8c4844ab0 100644
--- a/spec/requests/better_together/events_controller_spec.rb
+++ b/spec/requests/better_together/events_controller_spec.rb
@@ -87,7 +87,7 @@
describe 'RSVP actions' do
let(:user_email) { 'manager@example.test' }
- let(:password) { 'password12345' }
+ let(:password) { 'SecureTest123!@#' }
let(:event) do
BetterTogether::Event.create!(name: 'RSVP Test', starts_at: 1.day.from_now, identifier: SecureRandom.uuid)
end
diff --git a/spec/requests/better_together/joatu/agreements_spec.rb b/spec/requests/better_together/joatu/agreements_spec.rb
index b97987f2f..a870fdbe4 100644
--- a/spec/requests/better_together/joatu/agreements_spec.rb
+++ b/spec/requests/better_together/joatu/agreements_spec.rb
@@ -4,7 +4,7 @@
# rubocop:disable Metrics/BlockLength
RSpec.describe 'BetterTogether::Joatu::Agreements', :as_user do
- let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) }
+ let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) }
let(:person) { user.person }
let(:offer) { create(:joatu_offer) }
let(:request_record) { create(:joatu_request, creator: person) }
diff --git a/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb b/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb
index 765c82b5f..57de55d68 100644
--- a/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb
+++ b/spec/requests/better_together/joatu/offers_aggregated_matches_spec.rb
@@ -9,7 +9,7 @@
# Current authenticated user creating an offer
current_user = BetterTogether::User.find_by(email: 'user@example.test') ||
FactoryBot.create(:better_together_user, :confirmed,
- email: 'user@example.test', password: 'password12345')
+ email: 'user@example.test', password: 'SecureTest123!@#')
my_person = current_user.person
# Ensure both records share a category so they match
diff --git a/spec/requests/better_together/joatu/offers_spec.rb b/spec/requests/better_together/joatu/offers_spec.rb
index 4fb5c3864..885fec40f 100644
--- a/spec/requests/better_together/joatu/offers_spec.rb
+++ b/spec/requests/better_together/joatu/offers_spec.rb
@@ -4,7 +4,7 @@
# rubocop:disable Metrics/BlockLength
RSpec.describe 'BetterTogether::Joatu::Offers', :as_user do
- let(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) }
+ let(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) }
let(:person) { user.person }
let(:category) { create(:better_together_joatu_category) }
let(:valid_attributes) do
diff --git a/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb b/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb
index 45bfa4f0e..f53afbae4 100644
--- a/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb
+++ b/spec/requests/better_together/joatu/requests_aggregated_matches_spec.rb
@@ -9,7 +9,7 @@
# Current authenticated user creating a request
current_user = BetterTogether::User.find_by(email: 'user@example.test') ||
FactoryBot.create(:better_together_user, :confirmed,
- email: 'user@example.test', password: 'password12345')
+ email: 'user@example.test', password: 'SecureTest123!@#')
my_person = current_user.person
# Ensure both records share a category so they match
diff --git a/spec/requests/better_together/joatu/requests_spec.rb b/spec/requests/better_together/joatu/requests_spec.rb
index 7331d3536..9ab4227c7 100644
--- a/spec/requests/better_together/joatu/requests_spec.rb
+++ b/spec/requests/better_together/joatu/requests_spec.rb
@@ -7,7 +7,7 @@
include AutomaticTestConfiguration
let(:locale) { I18n.default_locale }
- let(:person) { find_or_create_test_user('user@example.test', 'password12345', :user).person }
+ let(:person) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user).person }
let(:category) { create(:better_together_joatu_category) }
let(:valid_attributes) do
{ name: 'New Request', description: 'Request description', creator_id: person.id,
diff --git a/spec/requests/better_together/joatu/response_links_controller_spec.rb b/spec/requests/better_together/joatu/response_links_controller_spec.rb
index 99dadc127..ce3a7a892 100644
--- a/spec/requests/better_together/joatu/response_links_controller_spec.rb
+++ b/spec/requests/better_together/joatu/response_links_controller_spec.rb
@@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe BetterTogether::Joatu::ResponseLinksController, :as_user do
- let(:user) { create(:user, :confirmed, password: 'password12345') }
+ let(:user) { create(:user, :confirmed, password: 'SecureTest123!@#') }
let(:person) { user.person }
let(:offer) { create(:better_together_joatu_offer, creator: person) }
let(:request_resource) { create(:better_together_joatu_request) }
diff --git a/spec/requests/better_together/notifications_controller_spec.rb b/spec/requests/better_together/notifications_controller_spec.rb
index 6c07dc624..8f87a39db 100644
--- a/spec/requests/better_together/notifications_controller_spec.rb
+++ b/spec/requests/better_together/notifications_controller_spec.rb
@@ -5,7 +5,7 @@
module BetterTogether # rubocop:todo Metrics/ModuleLength
RSpec.describe NotificationsController, :as_user do
let!(:notification) { create(:noticed_notification, recipient: person) }
- let!(:user) { find_or_create_test_user('user@example.test', 'password12345', :user) }
+ let!(:user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) }
let(:person) { user.person }
describe 'GET #index' do
diff --git a/spec/requests/better_together/person_checklist_items_spec.rb b/spec/requests/better_together/person_checklist_items_spec.rb
index 733d1110c..6fa54af03 100644
--- a/spec/requests/better_together/person_checklist_items_spec.rb
+++ b/spec/requests/better_together/person_checklist_items_spec.rb
@@ -13,8 +13,8 @@
before do
configure_host_platform
# Use project's HTTP login helper to satisfy route constraints
- test_user = find_or_create_test_user(user.email, 'password12345', :user)
- login(test_user.email, 'password12345')
+ test_user = find_or_create_test_user(user.email, 'SecureTest123!@#', :user)
+ login(test_user.email, 'SecureTest123!@#')
end
# rubocop:todo RSpec/MultipleExpectations
diff --git a/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb b/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb
index de42a5951..6c3fc6d48 100644
--- a/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb
+++ b/spec/requests/better_together/platform_privacy_with_event_invitations_spec.rb
@@ -7,8 +7,8 @@
let(:locale) { I18n.default_locale }
let!(:platform) { configure_host_platform }
- let!(:manager_user) { find_or_create_test_user('manager@example.test', 'password12345', :platform_manager) }
- let!(:regular_user) { find_or_create_test_user('user@example.test', 'password12345', :user) }
+ let!(:manager_user) { find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager) }
+ let!(:regular_user) { find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user) }
let!(:private_event) do
create(:better_together_event,
@@ -161,8 +161,8 @@
post user_registration_path(locale: locale), params: {
user: {
email: invitation.invitee_email,
- password: 'password12345',
- password_confirmation: 'password12345',
+ password: 'SecureTest123!@#',
+ password_confirmation: 'SecureTest123!@#',
person_attributes: {
name: 'New User',
identifier: 'newuser'
@@ -178,7 +178,7 @@
created_user = BetterTogether::User.find_by(email: invitation.invitee_email)
created_user.confirm
- login(invitation.invitee_email, 'password12345')
+ login(invitation.invitee_email, 'SecureTest123!@#')
# Should redirect to the event after successful registration. Compare by slug to avoid locale path differences
expect(response.request.fullpath).to include(event.slug)
diff --git a/spec/requests/better_together/users/registrations_spec.rb b/spec/requests/better_together/users/registrations_spec.rb
new file mode 100644
index 000000000..b8c35807f
--- /dev/null
+++ b/spec/requests/better_together/users/registrations_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'User Registration', :skip_host_setup do
+ include AutomaticTestConfiguration
+
+ before do
+ configure_host_platform
+ end
+
+ describe 'POST /en/users' do
+ let(:valid_user_params) do
+ {
+ email: 'test@example.com',
+ password: 'SecureTest123!@#',
+ password_confirmation: 'SecureTest123!@#',
+ person_attributes: {
+ name: 'Test User',
+ identifier: 'test-user'
+ }
+ }
+ end
+
+ let!(:privacy_agreement) do
+ BetterTogether::Agreement.find_or_create_by(identifier: 'privacy_policy') do |a|
+ a.title = 'Privacy Policy'
+ a.creator = create(:person)
+ end
+ end
+
+ let!(:terms_agreement) do
+ BetterTogether::Agreement.find_or_create_by(identifier: 'terms_of_service') do |a|
+ a.title = 'Terms of Service'
+ a.creator = create(:person)
+ end
+ end
+
+ let!(:code_of_conduct_agreement) do
+ BetterTogether::Agreement.find_or_create_by(identifier: 'code_of_conduct') do |a|
+ a.title = 'Code of Conduct'
+ a.creator = create(:person)
+ end
+ end
+
+ context 'when creating a user with person attributes' do
+ it 'creates user, person, and community membership successfully' do
+ expect do
+ post '/en/users', params: {
+ user: valid_user_params,
+ terms_of_service_agreement: '1',
+ privacy_policy_agreement: '1',
+ code_of_conduct_agreement: '1'
+ }
+ end.to change(BetterTogether::User, :count).by(1)
+ .and change(BetterTogether::Person, :count).by(1)
+ .and change(
+ BetterTogether::PersonCommunityMembership, :count
+ ).by(1)
+ .and change(
+ BetterTogether::AgreementParticipant, :count
+ ).by(3)
+
+ user = BetterTogether::User.last
+ expect(user.email).to eq('test@example.com')
+ expect(user.person).to be_present
+ expect(user.person.name).to eq('Test User')
+ expect(user.person.identifier).to eq('test-user')
+ expect(user.person.person_community_memberships.count).to eq(1)
+
+ user = BetterTogether::User.last
+ expect(user.email).to eq('test@example.com')
+ expect(user.person).to be_present
+ expect(user.person.name).to eq('Test User')
+ expect(user.person.identifier).to eq('test-user')
+ expect(user.person.person_community_memberships.count).to eq(1)
+ end
+
+ it 'handles person validation errors gracefully' do
+ invalid_params = valid_user_params.dup
+ invalid_params[:person_attributes][:name] = '' # Invalid empty name
+
+ expect do
+ post '/en/users', params: {
+ user: invalid_params,
+ terms_of_service_agreement: '1',
+ privacy_policy_agreement: '1',
+ code_of_conduct_agreement: '1'
+ }
+ end.to change(BetterTogether::User, :count).by(1) # User created despite empty name
+
+ expect(response).to have_http_status(:ok) # Form re-rendered with errors
+ end
+ end
+
+ context 'when agreements are not accepted' do
+ it 'does not create user or person' do
+ expect do
+ post '/en/users', params: {
+ user: valid_user_params
+ # No agreement checkboxes
+ }
+ end.not_to change(BetterTogether::User, :count)
+
+ expect(response).to have_http_status(:unprocessable_content) # Unprocessable entity
+ end
+ end
+ end
+end
diff --git a/spec/support/automatic_test_configuration.rb b/spec/support/automatic_test_configuration.rb
index 5a2c2c61a..7314c4b9d 100644
--- a/spec/support/automatic_test_configuration.rb
+++ b/spec/support/automatic_test_configuration.rb
@@ -93,7 +93,7 @@ def configure_host_platform
create(
:user, :confirmed, :platform_manager,
email: 'manager@example.test',
- password: 'password12345'
+ password: 'SecureTest123!@#'
)
end
@@ -166,9 +166,9 @@ def use_auth_method_for_spec_type(example, user_type)
if controller_spec_type?(example)
# Use Devise test helpers for controller specs
user = if user_type == :manager
- find_or_create_test_user('manager@example.test', 'password12345', :platform_manager)
+ find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager)
else
- find_or_create_test_user('user@example.test', 'password12345', :user)
+ find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user)
end
sign_in user
elsif feature_spec_type?(example)
@@ -176,7 +176,7 @@ def use_auth_method_for_spec_type(example, user_type)
extend BetterTogether::CapybaraFeatureHelpers unless respond_to?(:capybara_login_as_platform_manager)
# Ensure the target user exists before attempting a UI login
if user_type == :manager
- find_or_create_test_user('manager@example.test', 'password12345', :platform_manager)
+ find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager)
capybara_login_as_platform_manager
# Navigate to context-appropriate page when helpful
full_description = [
@@ -187,15 +187,15 @@ def use_auth_method_for_spec_type(example, user_type)
visit new_conversation_path(locale: I18n.default_locale)
end
else
- find_or_create_test_user('user@example.test', 'password12345', :user)
+ find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user)
capybara_login_as_user
end
else
# Request specs: choose auth mechanism based on description
user = if user_type == :manager
- find_or_create_test_user('manager@example.test', 'password12345', :platform_manager)
+ find_or_create_test_user('manager@example.test', 'SecureTest123!@#', :platform_manager)
else
- find_or_create_test_user('user@example.test', 'password12345', :user)
+ find_or_create_test_user('user@example.test', 'SecureTest123!@#', :user)
end
full_description = [
@@ -207,7 +207,7 @@ def use_auth_method_for_spec_type(example, user_type)
if full_description.include?('Example Automatic Configuration') && respond_to?(:sign_in)
sign_in user
else
- login(user.email, 'password12345')
+ login(user.email, 'SecureTest123!@#')
end
end
end
diff --git a/spec/support/better_together/capybara_feature_helpers.rb b/spec/support/better_together/capybara_feature_helpers.rb
index 7fa440e3c..6cb5bfa98 100644
--- a/spec/support/better_together/capybara_feature_helpers.rb
+++ b/spec/support/better_together/capybara_feature_helpers.rb
@@ -25,7 +25,7 @@ def configure_host_platform
create(
:user, :confirmed, :platform_manager,
email: 'manager@example.test',
- password: 'password12345'
+ password: 'SecureTest123!@#'
)
end
@@ -34,11 +34,11 @@ def configure_host_platform
# rubocop:enable Metrics/MethodLength
def capybara_login_as_platform_manager
- capybara_sign_in_user('manager@example.test', 'password12345')
+ capybara_sign_in_user('manager@example.test', 'SecureTest123!@#')
end
def capybara_login_as_user
- capybara_sign_in_user('user@example.test', 'password12345')
+ capybara_sign_in_user('user@example.test', 'SecureTest123!@#')
end
# rubocop:todo Metrics/PerceivedComplexity
diff --git a/spec/support/better_together/devise_session_helpers.rb b/spec/support/better_together/devise_session_helpers.rb
index 5f323218b..fca1beef9 100644
--- a/spec/support/better_together/devise_session_helpers.rb
+++ b/spec/support/better_together/devise_session_helpers.rb
@@ -25,7 +25,7 @@ def configure_host_platform
create(
:user, :confirmed, :platform_manager,
email: 'manager@example.test',
- password: 'password12345'
+ password: 'SecureTest123!@#'
)
end
@@ -34,11 +34,11 @@ def configure_host_platform
# rubocop:enable Metrics/MethodLength
def capybara_login_as_platform_manager
- capybara_sign_in_user('manager@example.test', 'password12345')
+ capybara_sign_in_user('manager@example.test', 'SecureTest123!@#')
end
def capybara_login_as_user
- capybara_sign_in_user('user@example.test', 'password12345')
+ capybara_sign_in_user('user@example.test', 'SecureTest123!@#')
end
def capybara_sign_in_user(email, password)
diff --git a/spec/support/request_spec_helper.rb b/spec/support/request_spec_helper.rb
index d6a6b85f6..7979ba5dc 100644
--- a/spec/support/request_spec_helper.rb
+++ b/spec/support/request_spec_helper.rb
@@ -90,7 +90,7 @@ def configure_host_platform # rubocop:disable Metrics/MethodLength
create(
:user, :confirmed, :platform_manager,
email: 'manager@example.test',
- password: 'password12345'
+ password: 'SecureTest123!@#'
)
end
diff --git a/spec/support/test_seed_helpers.rb b/spec/support/test_seed_helpers.rb
index 42871b82d..8af4f3f04 100644
--- a/spec/support/test_seed_helpers.rb
+++ b/spec/support/test_seed_helpers.rb
@@ -8,12 +8,12 @@
unless BetterTogether::User.find_by(email: manager_email)
person = BetterTogether::Person.create!(name: 'Manager Person')
- BetterTogether::User.create!(email: manager_email, password: 'testpassword12', person: person)
+ BetterTogether::User.create!(email: manager_email, password: 'SecureTest123!@#', person: person)
end
unless BetterTogether::User.find_by(email: user_email)
person = BetterTogether::Person.create!(name: 'Test User')
- BetterTogether::User.create!(email: user_email, password: 'testpassword12', person: person)
+ BetterTogether::User.create!(email: user_email, password: 'SecureTest123!@#', person: person)
end
end
end